Comment éviter l'erreur max_connections lors de la connexion d'AWS Aurora Postgres depuis Vercel
La plupart des tutoriels sur les infrastructures serverless se contentent de vous montrer une procédure en quelques clics pour vous connecter. Le problème survient juste après. Dès que vous déployez le code et qu'un léger pic de trafic arrive, les connexions à la base de données explosent. Vous commencez à suer à grosses gouttes en gérant des Access Keys statiques, ou vous recevez une facture de plusieurs centaines de dollars en frais de transfert de données.
Voici un récapitulatif des goulots d'étranglement rencontrés sur le terrain lors de l'interconnexion d'un frontend Vercel et d'un backend AWS, accompagnés de leurs solutions.
1. Configuration du pooling de connexions entre Vercel et AWS Aurora Postgres
PostgreSQL lance un nouveau processus chaque fois qu'un client se connecte. Chaque processus consomme entre 2 Mo et plus de 8 Mo de mémoire. Comme les fonctions serverless de Vercel créent des instances sans état de manière illimitée lorsqu'une requête arrive, la limite de max_connections d'Aurora Postgres (100 à 300 connexions) est atteinte en seulement quelques secondes.
Finalement, l'erreur FATAL: sorry, too many clients already apparaît, l'utilisation du CPU de la base de données atteint 100 % et le service s'effondre.
Vous devez placer PgBouncer entre les fonctions serverless et Aurora, et activer le mode Transaction Pooling (Pooling de transactions). Le pooling de sessions conserve les connexions sur un ratio 1:1, ce qui n'aide en rien, et le pooling de requêtes (statement pooling) ne permet pas d'utiliser des transactions multi-requêtes. Le transaction pooling n'attribue une connexion à la base de données que pendant l'exécution de la transaction de la requête, puis la renvoie immédiatement au pool dès que le COMMIT est émis.
Voici la configuration de référence pour le fichier pgbouncer.ini :
`ini
[pgbouncer]
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
reserve_pool_size = 10
max_db_connections = 80
`
- Activez la réutilisation à l'échelle de la transaction avec
pool_mode = transaction.
- Définissez
max_client_conn aux alentours de 10000 et augmentez la limite ulimit -n de votre OS.
default_pool_size correspond au nombre de connexions à maintenir ouvertes vers Aurora par couple utilisateur/base de données. Fixez une valeur entre 10 et 20.
- En fixant strictement
max_db_connections à 80, vous contrôlez de manière forcée la limite supérieure des connexions réelles entrant dans l'instance Aurora.
Dans l'environnement Fluid Compute de Vercel, plusieurs appels d'exécution partagent la portée globale (global scope). Vous devez donc déclarer le pool du driver DB dans le module scope. L'utilisation de attachDatabasePool du package @vercel/functions permet de nettoyer proprement les connexions inactives avant l'arrêt de l'instance de la fonction.
`typescript
import { Pool } from 'pg';
import { attachDatabasePool } from '@vercel/functions';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 5000,
});
attachDatabasePool(pool);
export default pool;
`
La chaîne de connexion doit également être séparée selon l'environnement. En développement local (.env.local), connectez-vous directement au port Aurora (5432) ou pointez vers un PgBouncer local. En production (.env.production), ciblez le port PgBouncer (6432) et ajoutez le paramètre pgbouncer=true.
`bash
.env.local (Direct Connection)
DATABASE_URL="postgresql://dbuser:dbpassword@aurora-cluster.us-east-1.rds.amazonaws.com:5432/app_dev?sslmode=require"
.env.production (PgBouncer)
DATABASE_URL="postgresql://dbuser:dbpassword@pgbouncer-proxy.internal:6432/app_prod?sslmode=require&pgbouncer=true"
`
2. Gérer les autorisations IAM via OIDC au lieu des Access Keys
Inscrire directement AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY dans les variables d'environnement Vercel est dangereux. Si les clés fuient, toute votre infrastructure est compromise. Si vous aviez accordé des privilèges d'administrateur tels que AdministratorAccess, la situation devient encore plus critique.
Utilisez OIDC (OpenID Connect). Cette méthode consiste à envoyer le JWT signé émis par Vercel à l'API AssumeRoleWithWebIdentity d'AWS STS pour obtenir des identifiants temporaires valables 1 heure. Les clés codées en dur disparaissent complètement.
Dans la politique de relation de confiance du rôle IAM AWS, définissez uniquement le privilège minimum.
`json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.vercel.com/my-team-slug"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.vercel.com/my-team-slug:aud": "https://vercel.com/my-team-slug",
"oidc.vercel.com/my-team-slug:sub": "owner:my-team-slug:project:my-ai-app:environment:production"
}
}
}
]
}
`
- Enregistrez
[oidc.vercel.com/my-team-slug](https://oidc.vercel.com/my-team-slug) dans l'IAM Identity Provider.
- Restreignez la revendication
aud à l'URL de votre équipe Vercel pour empêcher tout accès par une autre organisation.
- Visez précisément le nom du projet (
my-ai-app) et l'environnement (production) avec la condition sur la revendication sub. C'est le paramètre clé pour bloquer les attaques de type Confused Deputy.
Concernant la politique d'autorisations, n'ouvrez que les ressources nécessaires. Voici un exemple accordant uniquement le droit d'appeler une fonction Lambda et un bucket S3 spécifique.
`json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictedS3BucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-production-ai-assets",
"arn:aws:s3:::my-production-ai-assets/*"
]
},
{
"Sid": "RestrictedLambdaInvocation",
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:us-east-1:123456789012:function:python-ml-inference-service"
]
}
]
}
`
Dans le code Node.js, utilisez @vercel/oidc-aws-credentials-provider pour récupérer les identifiants temporaires.
`typescript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
const credentials = awsCredentialsProvider({
roleArn: process.env.AWS_ROLE_ARN!,
});
const s3Client = new S3Client({ region: 'us-east-1', credentials });
const lambdaClient = new LambdaClient({ region: 'us-east-1', credentials });
export async function POST(req: Request) {
const body = await req.json();
await s3Client.send(new PutObjectCommand({
Bucket: 'my-production-ai-assets',
Key: inputs/${Date.now()}.json,
Body: JSON.stringify(body),
}));
const lambdaRes = await lambdaClient.send(new InvokeCommand({
FunctionName: 'python-ml-inference-service',
Payload: Buffer.from(JSON.stringify(body)),
}));
return Response.json({
status: 'success',
result: JSON.parse(Buffer.from(lambdaRes.Payload!).toString()),
});
}
`
3. Réduire la latence de l'API due à l'éloignement des régions
Si le frontend Vercel tourne sur un PoP à Tokyo et que le backend AWS se trouve dans l'est des États-Unis (us-east-1), une latence physique de 150 ms à 250 ms s'ajoute à chaque aller-retour de requête.
Il est préférable de mettre en cache les réponses d'API qui changent rarement au niveau du Vercel Edge Middleware, afin d'éviter d'atteindre le backend.
`typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export const config = {
matcher: ['/api/v1/ml-models/:path*'],
};
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=120'
);
response.headers.set(
'Vercel-CDN-Cache-Control',
's-maxage=300, stale-while-revalidate=600'
);
return response;
}
`
En définissant s-maxage=60, stale-while-revalidate=120, le cache Edge répond immédiatement pendant 60 secondes. Une fois le cache expiré et jusqu'à 120 secondes, il renvoie d'abord une réponse obsolète tout en rafraîchissant le cache en arrière-plan.
Pour connecter le traçage entre les services, vous devez propager la spécification W3C TraceContext d'OpenTelemetry (en-tête traceparent). Voici la configuration Vercel :
`typescript
// instrumentation.ts
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: 'vercel-frontend-service',
instrumentationConfig: {
fetch: {
propagateContextUrls: ['api.my-aws-backend.com', '*.amazonaws.com'],
},
},
});
}
`
En ajoutant également le récepteur OpenTelemetry côté AWS FastAPI, le frontend et le backend seront reliés par un Trace ID unique.
`python
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
app = FastAPI(title="Python ML Service")
FastAPIInstrumentor.instrument_app(app)
@app.get("/api/v1/ml-models/predict")
async def predict():
tracer = trace.get_tracer(name)
with tracer.start_as_current_span("ml_inference_execution"):
return {"status": "completed", "prediction": [0.95, 0.05]}
`
4. Prévenir les frais de transfert de données et le coût du scaling illimité
Les dérapages de coûts surviennent généralement à deux endroits : les frais de dépassement Fast Data Transfer de Vercel (0,15 $ par Go au-delà de 1 To) et les frais Data Transfer Out d'AWS (0,09 $ par Go). Si les fonctions serverless et Lambda commencent à passer à l'échelle simultanément lors d'un pic de trafic, le montant de la facture change d'échelle.
Voici un exemple de Route API qui reçoit un webhook de limite de dépenses Vercel et envoie une notification sur Slack. Il est essentiel de vérifier la signature HMAC SHA1 pour des raisons de sécurité.
`typescript
import crypto from 'crypto';
export async function POST(req: Request) {
const payload = await req.text();
const signature = req.headers.get('x-vercel-signature');
const expectedSignature = crypto
.createHmac('sha1', process.env.VERCEL_SPEND_WEBHOOK_SECRET!)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
return new Response('Invalid Signature', { status: 401 });
}
const event = JSON.parse(payload);
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: Alerte de dépenses Vercel : limite budgétaire (${event.payload.spendAmount} USD) atteinte.,
}),
});
return new Response('OK', { status: 200 });
}
`
Assurez-vous d'activer ces deux garde-fous :
- Dans votre projet Vercel, sous Settings > Billing, activez Pause production deployment. Même si le service s'arrête lorsque le budget plafond est atteint, vous préserverez votre solde bancaire.
- Limitez la Reserved Concurrency (concurrence réservée) d'AWS Lambda aux alentours de 50. Cela empêche les instances de se multiplier indéfiniment, d'asphyxier la base de données et de générer des coûts massifs simultanés.