Guide · AWS Aurora
MCP Server Aurora Serverless v2 — ACU scaling, connection limits, session state
Aurora Serverless v2 is a strong fit for MCP server session state — it scales capacity in ~1 second in response to load spikes — but three things surprise teams coming from provisioned RDS or Aurora Serverless v1. First, connection limits are based on current ACU capacity, not maximum ACU: if your cluster is scaled down to 0.5 ACU at idle (the minimum ACU setting), the PostgreSQL max_connections is approximately 90 connections — a pre-warmed Lambda pool that tries to open 200 connections at idle will exhaust the limit before the cluster has time to scale up and expand its connection budget. Second, Aurora Serverless v2 cannot scale to zero: the minimum ACU is 0.5 (in regions that support it, 0 minimum is not available on v2 — use v1 or DynamoDB if you need true scale-to-zero with no idle cost). Third, scale-down is slow and leaves you paying for unused capacity: Aurora v2 scales down in 5-minute increments, taking 15-30 minutes to fully scale down after a traffic spike — during that window, you pay for the peak capacity whether or not you're using it.
TL;DR
Use Aurora Serverless v2 for MCP session state when you expect burst traffic patterns (low baseline, periodic spikes). Set the minimum ACU to accommodate your idle connection pool needs. Put RDS Proxy in front of Aurora v2 — the proxy decouples Lambda connection counts from Aurora's current connection limit, and prevents the connection limit cliff during scale-up. Do not rely on Aurora v2 for zero-cost idle periods; use DynamoDB or Aurora Serverless v1 if scale-to-zero matters.
Aurora Serverless v2 vs v1 vs provisioned: choosing for MCP
| Aspect | Aurora Serverless v1 | Aurora Serverless v2 | Aurora Provisioned |
|---|---|---|---|
| Minimum capacity | 0 ACU (scale to zero) | 0.5 ACU minimum (no zero) | Fixed — always running |
| Scale-up latency | 15-30 seconds (cold start from zero) | ~1 second (always warm) | Not applicable |
| Scale-down granularity | ACU steps, slow | 5-minute increments, 15-30 min to fully scale down | Not applicable |
| Idle cost | $0 if scaled to zero | ~$0.06/hour at 0.5 ACU minimum (PostgreSQL compatible) | Full instance cost at all times |
| Connection limits | Based on current ACU | Based on current ACU (same formula) | Fixed, based on instance type |
| VPC required | Yes (or Data API for no-VPC) | Yes | Yes |
| Best for MCP | Extremely low-traffic, cost-sensitive | Burst traffic with sub-second scale-up | Predictable high traffic, latency-sensitive |
ACU capacity and connection limits
Each Aurora Capacity Unit (ACU) provides approximately 2 GB of memory and 2 vCPUs. Aurora computes max_connections dynamically based on available memory using this formula for PostgreSQL:
max_connections ≈ LEAST( floor(memory_in_bytes / 9531392), 5000 )
-- For Aurora PostgreSQL:
-- 0.5 ACU (1 GB RAM) → ~90 connections
-- 1 ACU (2 GB RAM) → ~190 connections
-- 2 ACU (4 GB RAM) → ~390 connections
-- 4 ACU (8 GB RAM) → ~800 connections
-- 8 ACU (16 GB RAM) → ~1600 connections
-- 16 ACU (32 GB RAM) → ~3200 connections
The connection limit cliff: if your cluster is at 0.5 ACU (idle) and a burst of 200 Lambda invocations all try to connect simultaneously, each trying to open one connection to Aurora, the 91st connection fails with FATAL: remaining connection slots are reserved for non-replication superuser connections. Aurora will scale up in ~1 second, but the connections that failed during that 1-second window are already gone. The Lambda invocations that got rejected must retry — and by the time they retry, Aurora has scaled up and can accept more connections.
This is why RDS Proxy is important even with Aurora Serverless v2: the proxy holds a stable pool of connections to Aurora, and Lambda invocations connect to the proxy. The proxy queues connection requests if Aurora hasn't scaled up yet, rather than immediately returning an error to Lambda.
CDK configuration for Aurora Serverless v2
import * as rds from "aws-cdk-lib/aws-rds";
import * as ec2 from "aws-cdk-lib/aws-ec2";
const cluster = new rds.DatabaseCluster(this, "McpSessionDb", {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_15_4,
}),
serverlessV2MinCapacity: 0.5, // ~90 connections at idle; minimum in most regions
serverlessV2MaxCapacity: 16, // ~3200 connections at peak; adjust based on load
writer: rds.ClusterInstance.serverlessV2("writer", {
// Scale-up target: Aurora scales when CPU exceeds 40% or connection count rises
scaleWithWriter: true,
}),
readers: [
// Optional: add a reader for read-heavy MCP tools
rds.ClusterInstance.serverlessV2("reader", {
scaleWithWriter: false, // Reader scales independently
}),
],
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [dbSecurityGroup],
defaultDatabaseName: "mcpdb",
storageEncrypted: true,
backup: { retention: Duration.days(7) },
// Enable IAM authentication for passwordless Lambda connections
iamAuthentication: true,
});
Schema for MCP session state
Aurora is well-suited for storing MCP session state when the state requires relational integrity, ACID transactions, or complex queries across sessions. A minimal schema for MCP session state:
-- Sessions table: one row per active MCP session
CREATE TABLE mcp_sessions (
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_ping_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB,
-- Index for cleanup queries (purge sessions older than X hours)
CONSTRAINT check_last_ping CHECK (last_ping_at >= created_at)
);
CREATE INDEX idx_sessions_last_ping ON mcp_sessions (last_ping_at);
-- Tool call log: records each tool invocation for observability
CREATE TABLE mcp_tool_calls (
id BIGSERIAL PRIMARY KEY,
session_id UUID NOT NULL REFERENCES mcp_sessions(session_id) ON DELETE CASCADE,
tool_name TEXT NOT NULL,
called_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_ms INTEGER,
result_ok BOOLEAN NOT NULL,
error_msg TEXT
);
CREATE INDEX idx_tool_calls_session ON mcp_tool_calls (session_id, called_at DESC);
-- Automatic cleanup: delete sessions idle for more than 2 hours
-- Run via pg_cron or a scheduled Lambda
DELETE FROM mcp_sessions
WHERE last_ping_at < NOW() - INTERVAL '2 hours';
Monitoring Aurora Serverless v2 scaling
Key CloudWatch metrics to track for Aurora Serverless v2 in MCP workloads:
| Metric | What it tells you | Alert threshold |
|---|---|---|
ServerlessDatabaseCapacity | Current ACU capacity — watch for unexpected scale-up events indicating connection or CPU pressure | Alert when near serverlessV2MaxCapacity (capacity ceiling hit) |
DatabaseConnections | Active connections to Aurora — compare against connection limit for current ACU | Alert when > 80% of estimated max_connections for current ACU |
CPUUtilization | CPU load — high CPU triggers scale-up; consistently low CPU on the minimum ACU means cluster could have a lower minimum | Alert when sustained > 80% (scale-up may lag behind CPU spike) |
ACUUtilization | Percentage of current ACU capacity being used — 100% means you need a higher max ACU | Alert when sustained > 90% |
CommitLatency | Latency of COMMIT statements — spikes indicate write I/O pressure or lock contention in MCP session state operations | Alert when p99 > 100ms for session state writes |
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Connection failures during traffic spike despite Aurora scaling | Lambda invocations raced ahead of Aurora scale-up; the 1-second scale-up still results in connection rejections for connections attempted in that window | Put RDS Proxy in front of Aurora v2 — the proxy queues connection requests during the scale-up instead of immediately rejecting them; set connectionBorrowTimeout to 10-30s |
| Unexpectedly high cost at off-peak hours | Aurora v2 cannot scale below minimum ACU (0.5 ACU); if you expected scale-to-zero, you're paying for the minimum ACU 24/7 | Evaluate whether DynamoDB or Aurora Serverless v1 (which can scale to zero) is appropriate for your workload; for dev/test, use a scheduled shutdown via the RDS API |
| Scale-up happens but new connections still fail | After Aurora scales from 0.5 to 1 ACU, the new max_connections isn't instantly reflected in PostgreSQL — there may be a brief window where the limit hasn't been updated | Add retry logic with exponential backoff in the Lambda connection pool; 2-3 retries with 1s intervals covers the scale-up propagation delay |
| Cost spikes after a traffic burst — Aurora stays at peak capacity for 30+ minutes | Aurora v2 scales down in 5-minute increments and waits to ensure traffic doesn't return; a 15-minute burst can result in 30-45 minutes at peak capacity cost | This is expected behavior; budget for it by using a conservative max ACU and testing cost at sustained peak load; consider RDS Proxy's connection reuse to reduce how aggressively Aurora scales up |
| Reader replica connections fail after writer scales up | Reader and writer ACU scale independently; the reader may not scale up in sync with the writer if scaleWithWriter: false is set | Set scaleWithWriter: true for the reader to synchronize scaling; or set the reader's min ACU to match expected read load |