How to Prevent max_connections Errors When Connecting Vercel to AWS Aurora Postgres
TuBrief 편집팀
2026년 7월 21일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Most serverless infrastructure tutorials only show you the process of clicking a few connection buttons. The real problem comes after that. Once you deploy your code and traffic picks up even a little, DB connections get blown out, you sweat over managing static Access Keys, or you receive an invoice with hundreds of dollars in data transfer costs.
Here is a summary of practical bottlenecks and solutions when connecting a Vercel frontend with an AWS backend.
PostgreSQL spins up a new process every time a client connects. Each process consumes anywhere from 2MB to over 8MB of memory. Because Vercel serverless functions scale stateless instances endlessly upon receiving requests, they can exhaust Aurora Postgres's max_connections limit (100–300) in a matter of seconds.
Eventually, you encounter the FATAL: sorry, too many clients already error, DB CPU utilization hits 100%, and the service crashes.
You need to place PgBouncer between the serverless functions and Aurora and enable Transaction Pooling mode. Session pooling keeps connections on a 1:1 basis so it doesn't help, and statement pooling doesn't support multi-statement transactions. Transaction pooling allocates a DB connection strictly while a query transaction executes and returns it to the pool immediately upon COMMIT.
Here are the baseline settings for the pgbouncer.ini file:
`ini
[pgbouncer]
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
reserve_pool_size = 10
max_db_connections = 80
`
pool_mode = transaction.max_client_conn around 10000 and increase the OS ulimit -n limit.default_pool_size is the number of connections to keep open to Aurora per DB user and DB pair. Set it between 10 and 20.max_db_connections strictly at 80 forcefully controls the actual upper limit of connections going into the Aurora instance.In the Vercel Fluid Compute environment, multiple execution calls share global scope. Therefore, you must declare the DB driver pool at the module scope. Using attachDatabasePool from the @vercel/functions package cleanly cleans up idle connections before function instances shut down.
`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;
`
Connection strings should also be separated by environment. In the local development environment (.env.local), connect directly to the Aurora port (5432) or point to local PgBouncer, while in production (.env.production), use the PgBouncer port (6432) and append the pgbouncer=true parameter.
`bash
DATABASE_URL="postgresql://dbuser:dbpassword@aurora-cluster.us-east-1.rds.amazonaws.com:5432/app_dev?sslmode=require"
DATABASE_URL="postgresql://dbuser:dbpassword@pgbouncer-proxy.internal:6432/app_prod?sslmode=require&pgbouncer=true"
`
Hardcoding AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY directly into Vercel environment variables is risky. If the keys leak, your entire infrastructure is compromised. If you have granted administrative privileges like AdministratorAccess, the situation becomes even more severe.
Use OIDC (OpenID Connect). This method passes a Vercel-issued signed JWT to the AssumeRoleWithWebIdentity API of AWS STS to obtain temporary credentials valid for 1 hour. The hardcoded keys themselves disappear.
Only minimum required privileges should be defined in the AWS IAM Role trust relationship policy:
`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"
}
}
}
]
}
`
[oidc.vercel.com/my-team-slug](https://oidc.vercel.com/my-team-slug) in the IAM Identity Provider.aud claim to your Vercel team URL to prevent access from other organizations.my-ai-app) and environment (production) using the sub claim condition. This is a critical configuration to prevent Confused Deputy attacks.Permission policies should also only grant access to necessary resources. Here is an example granting access only to a specific S3 bucket and Lambda function invocation rights:
`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"
]
}
]
}
`
In Node.js code, retrieve temporary credentials using @vercel/oidc-aws-credentials-provider:
`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()),
});
}
`
If your Vercel frontend runs on a Tokyo PoP and your AWS backend is in US East (us-east-1), a physical latency of 150ms to 250ms is added to every round trip request.
For API responses that do not change frequently, it is best to cache them at the Vercel Edge Middleware layer to avoid passing the call to the backend entirely.
`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;
}
`
Setting s-maxage=60, stale-while-revalidate=120 serves responses immediately from the Edge cache for 60 seconds, and for up to 120 seconds after cache expiration, it serves the stale response while updating the cache in the background.
To connect cross-service tracing, you must propagate OpenTelemetry's W3C TraceContext standard (traceparent header). Here is the Vercel setup:
`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'],
},
},
});
}
`
Adding an OpenTelemetry receiver to the AWS FastAPI side similarly connects the execution path from frontend to backend with a single 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]}
`
Cost spikes usually happen in two places: Vercel's Fast Data Transfer overage fees (0.09 per GB). Add to this a traffic surge where serverless functions and Lambda start scaling concurrently, and your bill quickly reaches a whole new magnitude.
Here is an example API Route that receives a Vercel spend limit webhook and sends an alert to Slack. HMAC SHA1 signature verification is required to keep it secure:
`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: Vercel spend alert: Budget limit (${event.payload.spendAmount} USD) reached.,
}),
});
return new Response('OK', { status: 200 });
}
`
Make sure to turn on these two safety mechanisms: