Serverless Database Integration
The hardest problem in serverless architecture is database connectivity. Lambda functions spin up hundreds of concurrent instances, each opening its own database connection — overwhelming your PostgreSQL or MySQL server within minutes. We implement proper database integration patterns for serverless: RDS Proxy for connection pooling, DynamoDB for native serverless data, Prisma Accelerate for edge-compatible queries, and PlanetScale for serverless MySQL.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
The Connection Exhaustion Problem
A standard PostgreSQL instance supports 100–500 concurrent connections. A Lambda function under load can spawn 1,000 concurrent instances in seconds. Without connection pooling, your database crashes.
# Typical failure scenario:
# 1. Traffic spike → Lambda scales to 500 instances
# 2. Each instance opens 1 connection → 500 connections
# 3. PostgreSQL max_connections = 100 → 400 connections rejected
# 4. Error: "FATAL: too many connections for role"
# Solution: RDS Proxy sits between Lambda and RDS
# Lambda → RDS Proxy (pools connections) → RDS
# 500 Lambda instances → 20 actual database connections
resource "aws_db_proxy" "main" {
name = "${var.project}-proxy-${var.env}"
debug_logging = var.env != "production"
engine_family = "POSTGRESQL"
idle_client_timeout = 1800
require_tls = true
role_arn = aws_iam_role.rds_proxy.arn
vpc_security_group_ids = [aws_security_group.rds_proxy.id]
vpc_subnet_ids = var.private_subnet_ids
auth {
auth_scheme = "SECRETS"
iam_auth = "REQUIRED"
secret_arn = aws_secretsmanager_secret.db_credentials.arn
}
}
resource "aws_db_proxy_default_target_group" "main" {
db_proxy_name = aws_db_proxy.main.name
connection_pool_config {
max_connections_percent = 90
max_idle_connections_percent = 50
connection_borrow_timeout = 120
}
}RDS Proxy multiplexes hundreds of Lambda connections into a small pool of database connections. It handles connection reuse, health checks, and automatic failover to read replicas. We configure IAM authentication so Lambda functions connect to the proxy without storing database passwords.
DynamoDB for Serverless-Native Data
For new serverless applications, DynamoDB eliminates the connection problem entirely. It is an HTTP API — no connections to manage, no pool to configure, and it scales to millions of requests per second.
// DynamoDB client — reuse across invocations
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true },
});
// Single-table design — all entities in one table
export async function createProject(tenantId: string, project: Project) {
const now = new Date().toISOString();
await docClient.send(new PutCommand({
TableName: process.env.TABLE_NAME,
Item: {
PK: `TENANT#${tenantId}`,
SK: `PROJECT#${project.id}`,
GSI1PK: `PROJECT#${project.id}`,
GSI1SK: `META`,
type: 'project',
name: project.name,
status: 'active',
createdAt: now,
updatedAt: now,
},
ConditionExpression: 'attribute_not_exists(PK)',
}));
}
export async function listProjects(tenantId: string, cursor?: string) {
const result = await docClient.send(new QueryCommand({
TableName: process.env.TABLE_NAME,
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':pk': `TENANT#${tenantId}`,
':prefix': 'PROJECT#',
},
Limit: 25,
ExclusiveStartKey: cursor ? JSON.parse(Buffer.from(cursor, 'base64url').toString()) : undefined,
}));
return {
items: result.Items,
nextCursor: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64url')
: undefined,
};
}We initialize the DynamoDB client outside the handler function so it is reused across warm invocations. Single-table design with composite keys supports all access patterns through a single table, reducing costs and operational complexity.
Prisma with Serverless Adapters
If your application uses Prisma ORM, we configure it for serverless with the appropriate adapter to prevent connection exhaustion.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Lambda runtime
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// src/db.ts — Singleton Prisma client with Neon serverless adapter
import { Pool, neonConfig } from '@neondatabase/serverless';
import { PrismaNeon } from '@prisma/adapter-neon';
import { PrismaClient } from '@prisma/client';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
let prisma: PrismaClient;
export function getClient(): PrismaClient {
if (!prisma) {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
prisma = new PrismaClient({ adapter });
}
return prisma;
}
// Handler
export const handler = async (event: any) => {
const db = getClient();
const users = await db.user.findMany({ take: 10 });
return { statusCode: 200, body: JSON.stringify(users) };
};For Neon Postgres, the serverless driver uses WebSockets instead of TCP connections, enabling sub-10ms connection times from Lambda. For traditional RDS, we use RDS Proxy with Prisma's standard PostgreSQL adapter. The Prisma client is initialized once per Lambda instance and reused across invocations.
Connection Management Best Practices
Regardless of which database you use, these patterns prevent connection issues in serverless environments:
- Initialize outside the handler — Database clients created at module scope persist across warm invocations
- Set Lambda reserved concurrency — Cap the maximum instances to match your connection pool size
- Use connection timeouts — Set aggressive connect timeouts (3s) so functions fail fast instead of waiting for an exhausted pool
- Implement retry with backoff — Connection failures are transient; retry 3 times with exponential backoff
// Connection retry wrapper
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 100
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
if (attempt === maxRetries) throw err;
if (!isRetryable(err)) throw err;
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 100;
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Unreachable');
}
function isRetryable(err: any): boolean {
const msg = err.message || '';
return (
msg.includes('too many connections') ||
msg.includes('Connection timed out') ||
msg.includes('ECONNREFUSED') ||
err.code === 'ETIMEDOUT'
);
}
// Lambda concurrency guard
resource "aws_lambda_function" "api" {
reserved_concurrent_executions = 50 # Max 50 instances
# With RDS Proxy pooling 50 instances → ~20 DB connections
}We document the connection architecture in your runbook, including the relationship between Lambda concurrency limits, RDS Proxy pool size, and database max_connections. This ensures your team can adjust these values as traffic grows without hitting connection limits.
Why Anubiz Engineering
Ready to get started?
Skip the research. Tell us what you need, and we'll scope it, implement it, and hand it back — fully documented and production-ready.