Como evitar o erro max_connections ao conectar a Vercel ao AWS Aurora Postgres
A maioria dos tutoriais de infraestrutura serverless mostra apenas o processo de clicar em alguns botões de conexão. O problema vem a seguir. Assim que você faz o deploy do código e o tráfego aumenta um pouco, as conexões com o banco de dados caem, você começa a suar frio gerenciando Access Keys estáticas ou recebe uma fatura de centenas de dólares devido aos custos de transferência de dados.
Organizei aqui os gargalos práticos e as soluções que enfrentei ao integrar o front-end da Vercel com o back-end da AWS.
1. Configuração do Connection Pooling ao conectar Vercel e AWS Aurora Postgres
O PostgreSQL cria um novo processo para cada cliente conectado. Cada processo consome de 2 MB a mais de 8 MB de memória. Como as Serverless Functions da Vercel escalam instâncias stateless ilimitadamente à medida que as requisições chegam, o limite de max_connections do Aurora Postgres (100–300 conexões) é atingido em questão de segundos.
No fim, surge o erro FATAL: sorry, too many clients already, o uso de CPU do banco de dados bate 100% e o serviço cai.
É necessário colocar o PgBouncer entre as Serverless Functions e o Aurora e ativar o modo Transaction Pooling (Pooling de Transação). O Session Pooling mantém uma conexão 1:1, portanto não ajuda, e o Statement Pooling não suporta transações de múltiplas instruções. O Transaction Pooling aloca a conexão com o banco de dados apenas durante a execução da transação da query e a devolve ao pool imediatamente após o COMMIT.
Aqui estão os parâmetros recomendados para o arquivo pgbouncer.ini:
`ini
[pgbouncer]
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
reserve_pool_size = 10
max_db_connections = 80
`
- Ative a reutilização em nível de transação com
pool_mode = transaction.
- Defina
max_client_conn em cerca de 10000 e aumente o limite de ulimit -n do sistema operacional.
default_pool_size é o número de conexões a serem mantidas abertas no Aurora por par de usuário/banco de dados. Mantenha entre 10 e 20.
- Definir
max_db_connections exatamente em 80 força o controle do limite superior de conexões reais que entram na instância do Aurora.
No ambiente Fluid Compute da Vercel, múltiplas invocações de execução compartilham o escopo global. Portanto, você deve declarar o pool do driver do banco de dados no escopo do módulo. O uso da função attachDatabasePool do pacote @vercel/functions garante o encerramento correto de conexões ociosas antes que a instância da função seja desligada.
`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;
`
A string de conexão também deve ser separada por ambiente. No ambiente de desenvolvimento local (.env.local), conecte-se diretamente à porta do Aurora (5432) ou aponte para o PgBouncer local; em produção (.env.production), aponte para a porta do PgBouncer (6432) e adicione o 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. Gerenciando permissões do IAM via OIDC em vez de Access Keys
Inserir AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY diretamente nas variáveis de ambiente da Vercel é perigoso. Se a chave for vazada, toda a sua infraestrutura ficará comprometida. A situação é ainda pior se você tiver concedido permissões administrativas como AdministratorAccess.
Utilize OIDC (OpenID Connect). Esse método envia um JWT assinado emitido pela Vercel para a API AssumeRoleWithWebIdentity do AWS STS para obter credenciais temporárias válidas por 1 hora. Isso elimina totalmente a necessidade de ter chaves hardcoded.
A política de confiança da IAM Role na AWS deve definir apenas as permissões mínimas necessárias.
`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"
}
}
}
]
}
`
- Registre
[oidc.vercel.com/my-team-slug](https://oidc.vercel.com/my-team-slug) no IAM Identity Provider.
- Restrinja a claim
aud à URL da sua equipe na Vercel para impedir o acesso de outras organizações.
- Especifique o nome do projeto (
my-ai-app) e o ambiente (production) na condição da claim sub. Essa é a configuração chave para prevenir ataques do tipo Confused Deputy.
A política de permissão também deve liberar apenas os recursos necessários. Veja um exemplo que concede permissão apenas para um bucket S3 específico e invocações de funções 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"
]
}
]
}
`
No código Node.js, utilize @vercel/oidc-aws-credentials-provider para obter as credenciais temporárias.
`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. Reduzindo a latência de API causada pela diferença de regiões
Se o front-end da Vercel estiver rodando no PoP de Tóquio e o back-end da AWS estiver no leste dos EUA (us-east-1), haverá um acréscimo de 150 ms a 250 ms de latência física a cada requisição.
Para respostas de API que não mudam com frequência, o ideal é fazer o cache no Vercel Edge Middleware para evitar que a chamada chegue até o back-end.
`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;
}
`
Ao configurar s-maxage=60, stale-while-revalidate=120, a resposta é servida instantaneamente do cache Edge por 60 segundos. Após a expiração do cache, a versão antiga continua sendo servida por até 120 segundos enquanto o cache é atualizado em segundo plano.
Para conectar o rastreamento (tracing) entre serviços, é necessário propagar o padrão W3C TraceContext do OpenTelemetry (cabeçalho traceparent). Veja a configuração na 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'],
},
},
});
}
`
Ao adicionar um receptor OpenTelemetry equivalente no lado do FastAPI na AWS, o fluxo do front-end ao back-end é conectado sob um ú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. Prevenindo custos excessivos de transferência de dados e escalonamento ilimitado
Erros de custo geralmente acontecem em dois lugares: na taxa de excedente do Fast Data Transfer da Vercel (0,15porGBapoˊspassarde1TB)enastaxasdeDataTransferOutdaAWS(0,09 por GB). Somado a isso, se as Serverless Functions e o Lambda começarem a escalar simultaneamente durante picos de tráfego, o valor da fatura muda de patamar.
Aqui está um exemplo de API Route que recebe webhooks de limites de gastos da Vercel e envia notificações para o Slack. É importante validar a assinatura HMAC SHA1 para garantir a segurança.
`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 gastos na Vercel: Limite do orçamento (${event.payload.spendAmount} USD) atingido.,
}),
});
return new Response('OK', { status: 200 });
}
`
Certifique-se de ativar estas duas travas de segurança:
- Em Settings > Billing no seu projeto Vercel, ative Pause production deployment. Caso o limite do orçamento seja atingido, o serviço será pausado, mas o seu saldo bancário estará protegido.
- Limite a Reserved Concurrency (Concorrência Reservada) do AWS Lambda em cerca de 50. Isso evita que as instâncias escalem infinitamente, sobrecarregando o banco de dados e gerando cobranças massivas simultâneas.