Cómo evitar el error max_connections al conectar Vercel con AWS Aurora Postgres
La mayoría de los tutoriales de infraestructura serverless solo muestran un par de clics para conectar los servicios. El verdadero problema viene después. Una vez que despliegas el código y recibes un poco de tráfico, las conexiones a la base de datos se agotan, empiezas a sudar frío administrando Access Keys estáticas o te llega una factura con cientos de dólares por costos de transferencia de datos.
Hemos resumido los cuellos de botella prácticos y las soluciones al integrar el frontend de Vercel con el backend de AWS.
1. Configuración de Connection Pooling al conectar Vercel y AWS Aurora Postgres
PostgreSQL crea un nuevo proceso cada vez que un cliente se conecta. Cada proceso consume entre 2 MB y más de 8 MB de memoria. Dado que las funciones serverless de Vercel escalan instancias sin estado de forma ilimitada cuando entra una solicitud, pueden alcanzar el límite de max_connections de Aurora Postgres (de 100 a 300) en cuestión de segundos.
Al final, aparece el error FATAL: sorry, too many clients already, la CPU de la base de datos se dispara al 100% y el servicio se cae.
Es necesario colocar PgBouncer entre la función serverless y Aurora, y activar el modo Transaction Pooling. El Session Pooling mantiene las conexiones en una relación 1:1, por lo que no ayuda, y el Statement Pooling no permite transacciones con múltiples sentencias. El Transaction Pooling asigna una conexión a la base de datos únicamente mientras se ejecuta la transacción de la consulta y la devuelve al pool inmediatamente cuando se emite un COMMIT.
A continuación, se muestra la referencia de configuración para el archivo pgbouncer.ini.
`ini
[pgbouncer]
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
reserve_pool_size = 10
max_db_connections = 80
`
- Activa el reúso a nivel de transacción con
pool_mode = transaction.
- Establece
max_client_conn en alrededor de 10000 y aumenta el límite ulimit -n del sistema operativo.
default_pool_size es el número de conexiones que se mantendrán abiertas hacia Aurora por cada par usuario/base de datos. Ajusta este valor entre 10 y 20.
- Al fijar
max_db_connections en 80, puedes controlar de forma estricta el límite superior de conexiones reales que entran a la instancia de Aurora.
En el entorno Vercel Fluid Compute, múltiples ejecuciones comparten el scope global. Por lo tanto, debes declarar el pool del driver de la base de datos en el scope del módulo. Usar attachDatabasePool del paquete @vercel/functions garantiza que las conexiones inactivas se limpien correctamente antes de que la instancia de la función se apague.
`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 cadena de conexión también debe dividirse según el entorno. En el entorno de desarrollo local (.env.local), conéctate directamente al puerto de Aurora (5432) o apunta a un PgBouncer local. En producción (.env.production), utiliza el puerto de PgBouncer (6432) y añade el parámetro 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. Gestión de permisos IAM mediante OIDC en lugar de Access Keys
Ingresar directamente AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY en las variables de entorno de Vercel es peligroso. Si las llaves se filtran, toda la infraestructura queda expuesta. La situación es aún más grave si se han otorgado permisos de administrador como AdministratorAccess.
Utiliza OIDC (OpenID Connect). Con este método, Vercel emite un JWT firmado que se envía a la API AssumeRoleWithWebIdentity de AWS STS para obtener credenciales temporales con validez de 1 hora. Esto elimina por completo la necesidad de llaves hardcodeadas.
La política de relación de confianza del rol de AWS IAM debe definir únicamente los privilegios mínimos necesarios.
`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"
}
}
}
]
}
`
- Registra
[oidc.vercel.com/my-team-slug](https://oidc.vercel.com/my-team-slug) en el IAM Identity Provider.
- Restringe el claim
aud a la URL de tu equipo en Vercel para evitar que otras organizaciones accedan.
- Especifica con precisión el nombre del proyecto (
my-ai-app) y el entorno (production) mediante las condiciones del claim sub. Esta es la configuración clave para prevenir ataques de Confused Deputy.
La política de permisos también debe habilitar solo los recursos necesarios. A continuación se muestra un ejemplo que otorga acceso únicamente a un bucket de S3 específico y permiso de invocación para una función Lambda.
`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"
]
}
]
}
`
En el código de Node.js, utiliza @vercel/oidc-aws-credentials-provider para obtener credenciales temporales.
`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. Reducción de la latencia de las API debido a diferencias de región
Si el frontend de Vercel se ejecuta en un PoP de Tokio y el backend de AWS está en EE. UU. Este (us-east-1), se añadirá una latencia física de 150 ms a 250 ms en cada ida y vuelta de la solicitud.
Para las respuestas de la API que no cambian con frecuencia, lo ideal es almacenarlas en caché en el Edge Middleware de Vercel para evitar realizar la llamada al 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;
}
`
Al especificar s-maxage=60, stale-while-revalidate=120, la respuesta se entrega inmediatamente desde la caché Edge durante 60 segundos. Si la caché expira, se seguirá sirviendo la versión antigua durante los siguientes 120 segundos mientras la caché se actualiza en segundo plano.
Para conectar el rastreo entre servicios, se debe propagar el estándar W3C TraceContext (header traceparent) de OpenTelemetry. A continuación se muestra la configuración para 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 el lado de FastAPI de AWS, al agregar un receptor de OpenTelemetry del mismo modo, el frontend y el backend quedan vinculados mediante un único Trace ID.
`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. Prevención de cargos por transferencia de datos y escalado ilimitado
Los imprevistos de costos suelen ocurrir principalmente en dos lugares: los cargos por exceso en Fast Data Transfer de Vercel (0.15porGBdespueˊsde1TB)ylastarifasdeDataTransferOutenAWS(0.09 por GB). Si a esto se suma un pico de tráfico donde las funciones serverless y Lambda comienzan a escalar simultáneamente, el orden de magnitud de la factura cambiará drásticamente.
A continuación se muestra un ejemplo de API Route para recibir un webhook de límite de gasto de Vercel y enviar una notificación a Slack. Debe incluir verificación de firma HMAC SHA1 por seguridad.
`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: Alerta de gasto de Vercel: se ha alcanzado el límite del presupuesto (${event.payload.spendAmount} USD).,
}),
});
return new Response('OK', { status: 200 });
}
`
Asegúrate de activar estos dos mecanismos de seguridad:
- En tu proyecto de Vercel, ve a Settings > Billing y activa Pause production deployment. Esto protege el saldo de tu cuenta bancaria suspendiendo el servicio si se alcanza el límite de presupuesto.
- Limita la Reserved Concurrency (concurrencia reservada) de AWS Lambda a un valor cercano a 50. Esto evita que las instancias crezcan indefinidamente, sobrecarguen la base de datos y generen cargos masivos de manera simultánea.