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

AspectAurora Serverless v1Aurora Serverless v2Aurora Provisioned
Minimum capacity0 ACU (scale to zero)0.5 ACU minimum (no zero)Fixed — always running
Scale-up latency15-30 seconds (cold start from zero)~1 second (always warm)Not applicable
Scale-down granularityACU steps, slow5-minute increments, 15-30 min to fully scale downNot 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 limitsBased on current ACUBased on current ACU (same formula)Fixed, based on instance type
VPC requiredYes (or Data API for no-VPC)YesYes
Best for MCPExtremely low-traffic, cost-sensitiveBurst traffic with sub-second scale-upPredictable 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:

MetricWhat it tells youAlert threshold
ServerlessDatabaseCapacityCurrent ACU capacity — watch for unexpected scale-up events indicating connection or CPU pressureAlert when near serverlessV2MaxCapacity (capacity ceiling hit)
DatabaseConnectionsActive connections to Aurora — compare against connection limit for current ACUAlert when > 80% of estimated max_connections for current ACU
CPUUtilizationCPU load — high CPU triggers scale-up; consistently low CPU on the minimum ACU means cluster could have a lower minimumAlert when sustained > 80% (scale-up may lag behind CPU spike)
ACUUtilizationPercentage of current ACU capacity being used — 100% means you need a higher max ACUAlert when sustained > 90%
CommitLatencyLatency of COMMIT statements — spikes indicate write I/O pressure or lock contention in MCP session state operationsAlert when p99 > 100ms for session state writes

Common failure modes

SymptomCauseFix
Connection failures during traffic spike despite Aurora scalingLambda invocations raced ahead of Aurora scale-up; the 1-second scale-up still results in connection rejections for connections attempted in that windowPut 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 hoursAurora v2 cannot scale below minimum ACU (0.5 ACU); if you expected scale-to-zero, you're paying for the minimum ACU 24/7Evaluate 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 failAfter 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 updatedAdd 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+ minutesAurora 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 costThis 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 upReader and writer ACU scale independently; the reader may not scale up in sync with the writer if scaleWithWriter: false is setSet scaleWithWriter: true for the reader to synchronize scaling; or set the reader's min ACU to match expected read load