Guide · AWS RDS
MCP Server RDS Multi-AZ — failover impact on connection pools, DNS TTL, reconnect logic
RDS Multi-AZ provides automatic failover to a standby replica, but MCP servers face three specific challenges during the 60-120 second failover window. First, existing connection pool connections do not automatically migrate to the new primary: the pool holds TCP sockets to the old primary's IP address — when the primary goes down, those sockets receive Connection reset by peer or time out, and the pool must be drained and rebuilt against the new primary's IP. Second, Node.js's DNS resolver caches IP addresses at the OS level: even after the CNAME flips, pg.Pool uses the cached IP from the initial DNS lookup — new connection attempts in the pool may still try to reach the old IP for several minutes after failover unless DNS TTL is forced to be honored. Third, the MCP client receives errors during the failover window: rather than hanging the MCP tool call indefinitely, you should detect the DB error, return a structured MCP error response, and let the client retry — the failover window is predictable (60-120s) and should be communicated to the client, not hidden behind infinite retries.
TL;DR
Use RDS Proxy in front of Multi-AZ RDS — the proxy reconnects to the new primary transparently, and Lambda connection pools connect to the proxy endpoint, which never changes. If using direct connections, implement pool error detection that drains and recreates the pool on ECONNRESET or Connection terminated unexpectedly errors. Honor DNS TTL (5s for RDS endpoints) by not caching resolved IPs in application code.
The Multi-AZ failover timeline
RDS Multi-AZ maintains a hot standby in a different Availability Zone. When a failover event occurs (instance failure, AZ failure, or manual failover for maintenance), the following happens:
- 0s: Primary becomes unavailable. Existing connections begin failing with TCP errors. New connection attempts fail.
- ~20-60s: RDS detects the primary is down. The detection mechanism varies: hardware failures are detected in seconds; OS-level failures may take 30-60 seconds.
- ~60-120s: Standby is promoted to primary. The RDS endpoint DNS CNAME record is updated to point to the new primary's IP. TTL is 5 seconds.
- ~65-125s: After DNS propagation, new connections using the RDS endpoint reach the new primary. Existing connections in the pool still point to the old (dead) IP.
- After failover: Old primary may come back as the new standby, but the CNAME now points to the promoted instance.
| Failover trigger | Typical failover time |
|---|---|
| Hardware failure detected by RDS | 60–120 seconds |
Manual failover (aws rds reboot-db-instance --force-failover) | 60–120 seconds |
| AZ outage | 120–180 seconds (longer due to AZ-level detection) |
| OS-level crash on primary | 30–60 seconds (faster OS heartbeat detection) |
| Aurora Multi-AZ (same mechanic, different detection) | 30–60 seconds (Aurora uses cluster endpoint, not CNAME flip) |
Why connection pools don't automatically recover
A pg.Pool (or any connection pool) maintains a set of open TCP sockets. When Multi-AZ failover occurs, those sockets are connected to the primary's IP address. When the primary becomes unavailable, the OS-level TCP connection receives a RST or times out — the socket transitions to an error state.
The pool typically surfaces this error on the next query attempt as one of:
Error: Connection terminated unexpectedlyError: read ECONNRESETError: connect ETIMEDOUTError: terminating connection due to administrator command(PostgreSQL shutdown)
The pool may automatically remove the errored connection and create a new one — but here is the DNS caching problem: when it opens the new connection, it resolves the hostname again. If the OS DNS cache still holds the old IP (which it will for up to its TTL), the new connection also fails. Node.js's DNS resolution uses the OS resolver, which caches based on the TTL returned by the DNS server. RDS uses a 5-second TTL, but the OS may cache longer depending on the system's /etc/resolv.conf and ndots settings.
// This is the problem: pg.Pool reconnects using the cached IP
const pool = new Pool({
host: "mcp-prod.cluster-xxxx.us-east-1.rds.amazonaws.com",
// ... other options
});
// After failover, pool.connect() may resolve to the OLD IP
// if the OS DNS cache hasn't expired yet
const client = await pool.connect(); // Can fail even after CNAME flip
Pattern 1: Use RDS Proxy (eliminates the problem)
The recommended pattern for Lambda-based MCP servers is to use RDS Proxy. The proxy's endpoint is a stable hostname that never changes. The proxy itself handles reconnection to the new RDS primary after failover — the proxy reconnects in a matter of seconds, not minutes. Lambda connection pools connected to the proxy endpoint experience a much shorter outage (the proxy reconnection time, typically 5-30 seconds, versus the full 60-120 second RDS failover + DNS propagation window).
// Connect to the proxy endpoint, not the RDS cluster endpoint
const pool = new Pool({
host: process.env.RDS_PROXY_ENDPOINT,
// proxy-name.proxy-xxxx.us-east-1.rds.amazonaws.com
// This endpoint never changes, even across failovers
port: 5432,
database: process.env.DB_NAME,
// ...
});
Pattern 2: Pool drain-and-recreate on failover errors (direct connection)
If you connect directly to RDS without a proxy, implement a failover error handler that drains the pool and forces DNS re-resolution by creating a new pool instance.
import { Pool, DatabaseError } from "pg";
import dns from "dns/promises";
const DB_HOST = "mcp-prod.cluster-xxxx.us-east-1.rds.amazonaws.com";
function createPool(): Pool {
return new Pool({
host: DB_HOST,
port: 5432,
database: process.env.DB_NAME!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
ssl: { rejectUnauthorized: true },
max: 1,
connectionTimeoutMillis: 5_000,
idleTimeoutMillis: 10_000,
});
}
let pool = createPool();
let recreating = false;
function isFailoverError(err: Error): boolean {
return (
err.message.includes("Connection terminated unexpectedly") ||
err.message.includes("ECONNRESET") ||
err.message.includes("ETIMEDOUT") ||
err.message.includes("terminating connection due to administrator command") ||
// PostgreSQL error code 57P01: admin_shutdown
(err instanceof DatabaseError && err.code === "57P01")
);
}
async function drainAndRecreatePool(): Promise {
if (recreating) return; // Prevent concurrent recreation
recreating = true;
try {
// Wait for DNS TTL to expire before recreating (RDS TTL is 5s)
// Force DNS re-resolution: lookup the new IP before creating the pool
await new Promise((resolve) => setTimeout(resolve, 6_000));
const resolved = await dns.lookup(DB_HOST);
console.log(`DNS re-resolved to: ${resolved.address}`);
const oldPool = pool;
pool = createPool(); // New pool will use the freshly resolved IP
// End old pool (fire and forget — old connections are already dead)
oldPool.end().catch(() => {});
} finally {
recreating = false;
}
}
export async function query(sql: string, values?: any[]): Promise {
const client = await pool.connect();
try {
return await client.query(sql, values);
} catch (err) {
if (err instanceof Error && isFailoverError(err)) {
// Trigger async pool recreation; return error to MCP client immediately
drainAndRecreatePool().catch(console.error);
throw new Error(
"Database unavailable during failover. Retry in 30-60 seconds."
);
}
throw err;
} finally {
client.release();
}
}
Key decision: fail fast vs. wait. During a Multi-AZ failover (60-120s), you have two choices: queue incoming MCP tool calls and wait for the DB to recover, or immediately return an error and let the MCP client retry. For interactive MCP sessions, returning an error quickly is better UX than hanging for 60-120 seconds. The client's LLM can surface the error and suggest retrying — hanging connections appear broken to the client.
Subscribing to RDS failover events
For proactive notification, subscribe to the RDS event SNS topic and forward failover events to your MCP server's operational channel (PagerDuty, Slack, CloudWatch dashboard). Failover events allow you to take proactive action (e.g., temporarily rate-limit MCP tool calls that require DB access).
# Subscribe to RDS DB instance events (covers failover events)
aws rds create-event-subscription \
--subscription-name mcp-db-failover-alerts \
--sns-topic-arn arn:aws:sns:us-east-1:123456789012:mcp-ops-alerts \
--source-type db-instance \
--source-ids mcp-prod \
--event-categories '["failover","failure","availability"]' \
--enabled
# RDS will send SNS notifications for:
# - RDS-EVENT-0025: Multi-AZ failover initiated
# - RDS-EVENT-0049: Multi-AZ failover completed
# - RDS-EVENT-0013: Insufficient capacity in AZ
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| MCP tool calls hang for 60-120s during failover, then error | Connection pool holds stale sockets; connectionTimeoutMillis is not set, so new connection attempts wait indefinitely | Set connectionTimeoutMillis: 5000 on the pool; detect failover errors and return a fast MCP error response instead of waiting |
| Pool recreated after failover but new connections still fail | OS DNS cache still holds old IP; new pool connects to the dead primary's IP even after CNAME flip | Add a 6-second sleep before recreating the pool to allow the 5-second RDS DNS TTL to expire; or use dns.lookup() to verify the IP changed before creating the new pool |
| Some MCP sessions recover after failover, others stay broken | Lambda execution contexts have different pool states — some have already recycled the pool, others have not yet encountered an error | Use a module-level pool that all handlers share; the first error triggers recreation, and subsequent invocations use the new pool; avoid per-invocation pool creation |
| Failover completes but read-replica queries still route to old endpoint | Read replicas have separate endpoints; only the primary CNAME flips on failover — the reader endpoint is separate for Aurora clusters | For Aurora: use the cluster reader endpoint (cluster-name.cluster-ro-xxxx.rds.amazonaws.com) for read replicas; it auto-updates after failover; for RDS Multi-AZ with read replicas, each replica has its own endpoint |
| Manual failover test shows 3-5 minute outage instead of 60-120s | Application DNS cache is longer than RDS TTL; the application is not re-resolving the hostname | Verify no explicit DNS caching in application code; check that the pool recreates rather than reconnecting with the cached IP; consider using RDS Proxy to eliminate this issue entirely |