AWS Aurora · RDS · 2026-10-24 · Aurora/RDS advanced arc
Aurora and RDS Advanced Patterns for MCP Server Databases: Serverless v2, Global Database, Blue/Green Deployments, and Performance Insights
MCP server backends that grow beyond a single-region SQLite prototype and into production Aurora or RDS clusters hit four advanced operational problems in sequence, and solving them in the wrong order creates more work later. Aurora Serverless v2's continuous ACU scaling sounds like it eliminates capacity planning — it does not eliminate connection-limit planning: a Lambda-heavy MCP alert worker that bursts to 200 concurrent executions saturates the database's max_connections parameter before the ACU scale-up completes, requiring RDS Proxy in front of the cluster regardless of how generous the maxCapacity setting is; and the Data API that teams relied on in Aurora Serverless v1 is gone in v2, forcing a native-driver migration for any Lambda that used HTTP-based database access. Aurora Global Database's storage-level cross-region replication achieves sub-second RPO, but the two failover paths — managed planned failover and detach-and-promote — have incompatible data-loss profiles: managed planned failover guarantees zero data loss by stopping writes and draining replication lag before promoting the secondary, while detach-and-promote promotes immediately and accepts up to one second of data loss — using the wrong one in a DR scenario means either data loss you didn't expect or a 5-minute stall waiting for lag to drain while the primary is already down. RDS Blue/Green Deployments create a fully managed staging copy that replicates every write from production, giving teams a safe lab for schema changes — but the 60–90 second switchover write interruption is non-negotiable, the schema changes on green must be backward-compatible with the application code running against blue if you want rollback to be safe, and for PostgreSQL the source cluster needs rds.logical_replication = 1 set in a parameter group and applied (requiring a reboot) before creating the deployment. RDS Performance Insights' DBLoad metric is unitless and only alarms when it exceeds the vCPU count — a DBLoad of 3 on a two-vCPU instance is a crisis, but on an eight-vCPU instance it is headroom — and the wait event breakdown is the diagnostic layer that transforms "the database is slow" into "these specific probe insert queries are competing for the same row range, fix it by partitioning workers by server_slug prefix." This guide synthesises all four patterns. Pattern 1 covers Aurora Serverless v2 ACU configuration, the RDS Proxy necessity, Data API migration from v1, and CloudWatch monitoring — synthesised from Aurora Serverless v2 for MCP Servers — Continuous Scaling, ACU Configuration, RDS Proxy. Pattern 2 covers Aurora Global Database architecture, the two failover paths, write forwarding limitations, and replication lag monitoring — synthesised from Aurora Global Database for MCP Servers — Multi-Region Replication, Sub-Second RPO, Managed Failover. Pattern 3 covers RDS Blue/Green deployment prerequisites, the staging and switchover lifecycle, schema compatibility rules, and rollback — synthesised from RDS Blue/Green Deployments for MCP Servers — Zero-Downtime Schema Changes, Switchover, Rollback. Pattern 4 covers Performance Insights DBLoad interpretation, wait event taxonomy, Top SQL analysis, and the relationship between Enhanced Monitoring and PI — synthesised from RDS Performance Insights for MCP Servers — DB Load, Wait Events, Top SQL Analysis.
TL;DR
- Aurora Serverless v2 — ACU range, RDS Proxy for Lambda, Data API is gone: Set
engineMode: "provisioned"withserverlessV2ScalingConfiguration: { minCapacity: 2, maxCapacity: 32 }and instance classdb.serverless. UseminCapacity: 2for production to avoid 1–3 second cold-scale latency after quiet periods. Add RDS Proxy in front of the cluster when Lambda functions connect — Lambda burst can hitmax_connectionsbefore ACU scale-up responds. The Data API from Serverless v1 is not available on v2; migrate Lambda callers to RDS Proxy + native driver using IAM token auth. Monitor withServerlessDatabaseCapacity(current ACU float) andACUUtilization(percentage of max); alarm when either approaches the ceiling for 5+ minutes. SetscaleWithWriter: trueon reader instances so failover promotion doesn't catch the reader at minimum capacity. - Aurora Global Database — storage replication, two failover paths, write forwarding cost: Global Database uses storage-level (not engine-level) replication across up to 5 secondary regions, typically achieving under 1 second of lag even to distant regions. For planned maintenance failover, use
failover-global-clusterwith--allow-data-loss false— this stops writes on the primary, drains lag to zero, then promotes the target secondary in ~30–60 seconds with zero data loss. For DR when the primary is unreachable, useremove-from-global-cluster— the secondary is promoted immediately, accepting up to ~1 second of data loss. After any failover, update application connection strings; use Route 53 CNAME with 30-second TTL on the writer endpoint to make this application-transparent. Write forwarding (secondary-to-primary) adds 20–50 ms roundtrip latency per write — use it only for low-frequency administrative writes, not for high-throughput probe inserts. Alarm onAuroraGlobalDBReplicationLagat 5 seconds per secondary. - RDS Blue/Green Deployments — prerequisites, staging, switchover, schema compatibility: For PostgreSQL source clusters, set
rds.logical_replication = 1in the parameter group and apply it (requires reboot) before creating the deployment. Create the deployment withcreate-blue-green-deployment— AWS provisions a green replica and sets up managed replication from blue. Run schema migrations on the green endpoint while blue continues serving production traffic; test with a canary deployment pointed at green. Before switchover, verify replication lag is consistently below 100 ms. Executeswitchover-blue-green-deployment: writes pause on blue for ~60–90 seconds while lag drains, then DB identifiers swap and writes resume on the new writer. Schema changes on green must be backward-compatible with blue application code for rollback to remain safe. RDS Proxy handles the endpoint swap transparently. Rollback: delete the blue/green deployment resource, which leaves the old blue environment intact. - Performance Insights — DBLoad vs vCPU, wait events, Top SQL, Enhanced Monitoring: Enable PI on each DB instance (7-day free retention). DBLoad (Average Active Sessions) is only alarming when it exceeds the instance's vCPU count — a db.r7g.large has 2 vCPUs, so DBLoad > 2 means saturation. Decompose DBLoad by wait event to identify the bottleneck:
CPU= add indexes;Lock:relation= DDL competing with OLTP, run in maintenance window;Lock:tuple= row contention, partition workers by primary key range;IO:DataFileRead= cache miss, scale instance or increase IOPS;IO:BufFileRead= spill to disk, increasework_mem. Sort Top SQL by "DB load per call" to find the slowest queries, not execution count. Enable Enhanced Monitoring separately for OS-level CPU/memory/disk; use it alongside PI to distinguish compute bottleneck from I/O bottleneck. For RDS MySQL, enableperformance_schema = 1in the parameter group or PI captures load without query text.
Pattern 1 — Aurora Serverless v2: Continuous Scaling and the Connection-Limit Problem
Why v2 is architecturally different from v1
Aurora Serverless v1 used a step-function scaling model: the cluster scaled from 4 ACUs to 8 ACUs to 16 ACUs in discrete jumps, and each jump involved a brief pause during which connections were held. v2 replaced that with continuous fractional scaling — the cluster scales from 0.5 to 128 ACUs in sub-ACU increments with no pause. This makes v2 far more suitable for production MCP backends where probe write traffic is not completely uniform.
However, the architectural differences go deeper than scaling granularity:
| Feature | Aurora Serverless v1 | Aurora Serverless v2 |
|---|---|---|
| Scaling model | Step-function; brief pause at each step | Continuous fractional; no pause |
| Minimum capacity | 0 ACUs (can fully pause) | 0.5 ACUs minimum; never truly pauses |
| Data API | Supported — HTTP-based, no TCP pool needed | Not supported — must use native driver or RDS Proxy |
| Global Database | Not supported | Supported as secondary capacity type |
| Max capacity | 256 ACUs | 128 ACUs per instance |
| Engine version | Aurora MySQL 5.6/5.7; Aurora PostgreSQL 10/11 | Aurora MySQL 8.0 (3.x); Aurora PostgreSQL 13+ |
For MCP teams migrating from v1, the Data API removal is the most operationally disruptive change. Any Lambda function that used rds-data.ExecuteStatement to issue queries without maintaining a TCP connection pool must be rewritten. The two migration paths are RDS Proxy + IAM authentication (recommended for Lambda-heavy workloads) or a module-level connection pool in the Lambda function itself (acceptable for low-concurrency scenarios).
Setting minimum capacity for production latency
The minCapacity setting is the most consequential configuration decision for production MCP backends. Setting it to 0.5 (the minimum) means the cluster scales back to 0.5 ACUs after a sustained quiet period and must scale up again on the next request — this produces a 1–3 second delay on the first query after idle, which appears as a probe-worker timeout in the monitoring system. For production probe databases that must write a result row every 60 seconds for every monitored endpoint, set minCapacity: 2:
const cluster = new rds.DatabaseCluster(this, "McpProbeDb", {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_16_2,
}),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
writer: rds.ClusterInstance.serverlessV2("writer", {
performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT,
}),
readers: [
rds.ClusterInstance.serverlessV2("reader", {
scaleWithWriter: true, // reader capacity tracks writer — critical for safe failover
}),
],
serverlessV2MinCapacity: 2, // 0.5 for dev; 2+ for production to pre-warm connections
serverlessV2MaxCapacity: 32, // burst headroom for registry re-scans
iamAuthentication: true,
deletionProtection: true,
backup: { retention: cdk.Duration.days(7) },
});
Scale-down hysteresis works in your favor for bursty probe workloads: Aurora Serverless v2 requires 15 consecutive minutes of low utilization before scaling down. A registry re-scan that spikes the cluster to 16 ACUs for 3 minutes will scale back down slowly, keeping warm connections ready for the next re-scan rather than scaling to the floor and forcing a cold-start on the next burst.
RDS Proxy is mandatory for Lambda-heavy workloads
This is the most widely misunderstood aspect of Serverless v2: the connection limit scales with ACUs, but Lambda cold-start bursts happen faster than ACU scale-up responds. At 2 ACUs, Aurora PostgreSQL allows approximately 200 connections. When an MCP monitoring alert fires and triggers 200 concurrent Lambda alert-worker executions, each opening a native Aurora connection, the cluster hits max_connections before the scale-up event completes — the alert workers fail with "too many connections" even though the cluster has significant ACU headroom.
RDS Proxy solves this by maintaining a fixed-size native connection pool to the Aurora cluster and multiplexing application connections through it. Hundreds of Lambda executions each connect to the proxy (which is fast, connection-light, and immediately available), and the proxy manages the actual native connections to Aurora:
const proxy = new rds.DatabaseProxy(this, "McpDbProxy", {
proxyTarget: rds.ProxyTarget.fromCluster(cluster),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
iamAuth: true, // Lambda authenticates via IAM, not password
requireTLS: true,
idleClientTimeout: cdk.Duration.minutes(10),
maxConnectionsPercent: 70, // proxy claims 70%; reserve 30% for direct admin access
});
Lambda functions using IAM auth to the proxy do not need database passwords stored in Secrets Manager — they generate a short-lived IAM token using the @aws-sdk/rds-signer package and use it as the database password:
import { Signer } from "@aws-sdk/rds-signer";
import { Client } from "pg";
const signer = new Signer({
hostname: process.env.PROXY_ENDPOINT!,
port: 5432,
region: process.env.AWS_REGION!,
username: "lambda_user",
});
let client: Client | null = null;
export async function handler() {
if (!client || !client.connected) {
const token = await signer.getAuthToken(); // IAM token rotates every 15 min
client = new Client({
host: process.env.PROXY_ENDPOINT,
port: 5432,
database: "mcp_probe",
user: "lambda_user",
password: token,
ssl: { rejectUnauthorized: true },
});
await client.connect();
}
// reuse warm client across Lambda invocations
}
Monitoring: two Serverless v2-specific CloudWatch metrics
ServerlessDatabaseCapacity (current ACU count as a float) and ACUUtilization (percentage of maxCapacity consumed) are the primary Serverless v2 observability signals. Standard Aurora metrics like CPUUtilization still apply but are less directly actionable — ACU utilization is the primary capacity signal:
# Alarm: cluster pegged at max capacity for 5+ minutes = ceiling too low
aws cloudwatch put-metric-alarm \
--alarm-name "mcp-probe-db-capacity-ceiling" \
--namespace "AWS/RDS" \
--metric-name "ServerlessDatabaseCapacity" \
--dimensions Name=DBClusterIdentifier,Value=mcp-probe-db \
--statistic Maximum \
--period 60 \
--evaluation-periods 5 \
--threshold 31.5 \
--comparison-operator GreaterThanOrEqualToThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-ops-alerts
Pattern 2 — Aurora Global Database: Multi-Region Replication and the Two Failover Paths
Storage-level vs engine-level replication
Aurora Global Database uses dedicated replication infrastructure at the storage layer — below the database engine, running in parallel with the engine's normal I/O path. This is architecturally distinct from Aurora read replicas within a region (which use InnoDB/WAL-based replication through the engine) or RDS cross-region read replicas (which use binlog replication). Because the replication bypasses the engine, it achieves typical lag under 1 second between even distant regions (us-east-1 to ap-southeast-1 averages 200–400 ms lag), and the replication cost scales with write I/O volume rather than with query complexity.
For MCP monitoring platforms that need probe data accessible in multiple regions — either for read locality (lower-latency status dashboard for European users) or disaster recovery — Global Database is the right tool. Application-level replication (writing to two separate Aurora clusters independently) is operationally more complex, produces divergence risk, and cannot guarantee RPO.
The two failover paths and when to use each
This is the most operationally consequential distinction in Global Database: there are two distinct failover mechanisms with incompatible data-loss guarantees, and using the wrong one in a DR scenario either wastes minutes waiting for lag to drain from an unreachable primary or accepts unexpected data loss in a planned migration.
Managed planned failover (failover-global-cluster) — use when the primary region is healthy and you are performing a scheduled maintenance migration or a regional load rebalancing:
aws rds failover-global-cluster \
--global-cluster-identifier mcp-probe-global \
--target-db-cluster-identifier arn:aws:rds:eu-west-1:123456789012:cluster:mcp-probe-secondary-eu \
--allow-data-loss false
# Monitor: the primary stops accepting writes, lag drains to zero, secondary is promoted
# Old primary becomes a secondary after the operation (~30-60 seconds total)
aws rds describe-global-clusters \
--global-cluster-identifier mcp-probe-global \
--query "GlobalClusters[0].GlobalClusterMembers[*].{ARN:DBClusterArn,IsWriter:IsWriter}"
The API blocks writes on the primary, waits for AuroraGlobalDBReplicationLag to reach zero on the target secondary, then promotes. If the primary has long-running open transactions preventing lag drain, the operation stalls — terminate long-running transactions on the primary before initiating the failover.
Detach-and-promote (remove-from-global-cluster) — use only when the primary region is unreachable and you need to restore write capability in the secondary region. This operation does not wait for lag to drain:
# Use only in DR: promotes the secondary immediately with up to ~1 second data loss
aws rds remove-from-global-cluster \
--global-cluster-identifier mcp-probe-global \
--db-cluster-identifier arn:aws:rds:eu-west-1:123456789012:cluster:mcp-probe-secondary-eu \
--region eu-west-1
# The secondary is now a standalone Aurora cluster with write capability
# Any replication lag at promotion moment = data written to primary but not yet replicated = lost
After the primary region recovers, re-add the old primary as a new secondary to reconstitute the Global Database. Do not attempt to merge the diverged write histories — the detach creates a clean split point.
Write forwarding: the 50 ms tax
Global write forwarding lets an application connected to a secondary cluster issue writes that are forwarded to the primary region over the storage replication network. This is useful for occasional administrative writes (triggering a manual probe re-check, creating an alert acknowledgment) from secondary-region tooling, without requiring a separate connection to the primary region.
The tax is real: every forwarded write adds the full cross-region network roundtrip (20–50 ms for eu-west-1 to us-east-1). Do not route probe workers' high-frequency status inserts through write forwarding — the 50 ms per insert at 167 writes per second translates to steady-state latency that a direct connection to the primary's writer endpoint does not incur.
Route 53 CNAME for transparent endpoint switching
After any failover — planned or DR — application connection strings must be updated from the old primary's writer endpoint to the new one. The cleanest way to automate this is a Route 53 CNAME with a short TTL (30 seconds) pointing to the current primary's cluster writer endpoint:
# Before failover: db-writer.internal.example.com → mcp-probe-primary.cluster-xxx.us-east-1.rds.amazonaws.com (TTL 30s)
# After failover: update the CNAME to the new primary in eu-west-1
# Application sees the new endpoint within 30 seconds, no code change needed
aws route53 change-resource-record-sets \
--hosted-zone-id Z1234567890ABC \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "db-writer.internal.example.com",
"Type": "CNAME",
"TTL": 30,
"ResourceRecords": [{"Value": "mcp-probe-secondary-eu.cluster-yyy.eu-west-1.rds.amazonaws.com"}]
}
}]
}'
Pattern 3 — RDS Blue/Green Deployments: Safe Schema Migrations with a Managed Staging Database
What Blue/Green creates
RDS Blue/Green Deployments provision a complete replica of your production cluster (green) connected to production (blue) via a managed replication channel. During the staging period, green receives every write made to blue in near-real-time — it is a continuously synchronized mirror, not a point-in-time snapshot. This makes it safe to run schema migrations on green, test application behavior against the migrated schema, and discard green entirely if the migration looks wrong, all without touching the production database.
The lifecycle has four phases:
| Phase | Blue (production) | Green (staging) |
|---|---|---|
| Provisioning | Normal production traffic | Snapshot restore + initial replication catch-up (minutes to hours depending on data size) |
| Staging | Normal production traffic | Continuously replicating from blue; you apply schema changes and test here |
| Switchover (~60–90s) | Writes paused (write quorum stop); DB identifiers swap | Lag drains to zero; becomes the new production writer |
| Post-switchover | Now the old blue — read-only fallback; replication from new blue (old green) to old blue begins | Now the production writer |
Prerequisites: logical replication must be enabled before you create the deployment
For PostgreSQL source clusters, this is a reboot-required change that must happen in advance of the migration window. Setting rds.logical_replication = 1 in the parameter group does not take effect until the instance is rebooted — if you set it and try to create the Blue/Green deployment without rebooting, the deployment creation fails with "logical replication is not enabled":
# For PostgreSQL: enable logical replication in parameter group (requires reboot)
# Verify the parameter group is applied to the cluster
aws rds describe-db-clusters \
--db-cluster-identifier mcp-probe-db \
--query "DBClusters[0].DBClusterParameterGroup"
# Check the parameter value
aws rds describe-db-cluster-parameters \
--db-cluster-parameter-group-name mcp-probe-pg16-params \
--query "Parameters[?ParameterName=='rds.logical_replication'].{Name:ParameterName,Value:ParameterValue,ApplyType:ApplyType}"
# If ApplyType is "static", a reboot is required before the parameter takes effect
# Schedule the reboot during a low-traffic window BEFORE the migration sprint
# For MySQL/Aurora MySQL: binlog is typically enabled by default on Aurora MySQL 3.x
# Verify:
# SELECT @@binlog_format, @@log_bin; -- should return ROW, 1
# Create the deployment (after verifying prerequisites)
aws rds create-blue-green-deployment \
--blue-green-deployment-name mcp-probe-bgd-schema-v3 \
--source arn:aws:rds:us-east-1:123456789012:cluster:mcp-probe-db \
--target-engine-version "16.3" \ # optional: also upgrade engine version on green
--tags Key=Purpose,Value=SchemaV3Migration
Schema compatibility rule: the expand-contract pattern
The most important constraint for Blue/Green Deployments is one that is easy to miss: the schema changes you apply to green must be backward-compatible with the application code version currently running against blue. This matters for rollback. If you switch over to green, discover a problem, and want to roll back by deleting the blue/green deployment (which restores the old blue as writer), the application code that shipped for the green schema will now run against the old blue schema.
Backward-compatible changes (safe for rollback): adding a nullable column, adding an index, adding a table, widening a varchar, relaxing a constraint.
Backward-incompatible changes (rollback is not safe): dropping a column, renaming a column, adding a NOT NULL column without a default, narrowing a type. If you must make an incompatible change, use the expand-contract pattern: in one deployment, add the new column alongside the old one and ship application code that writes to both; in a second deployment, drop the old column after all readers are confirmed migrated.
Switchover execution and the 90-second gap
The switchover itself is non-negotiable: applications experience approximately 60–90 seconds of write interruption for Aurora, and up to 3 minutes for large RDS MySQL instances. During this window, the probe workers' INSERT statements queue at the connection pool and resume automatically after the identifier swap completes — but the uptime timeline has a gap. AliveMCP handles this by recording a "monitoring interrupted — database migration in progress" status for any endpoint whose probe result falls within the switchover window, rather than recording a false outage.
# Pre-switchover checklist:
# 1. Replication lag < 100ms consistently for 10+ minutes
# 2. Application code tested against green and ready for the new schema
# 3. Maintenance window selected (low MCP endpoint traffic)
# 4. RDS Proxy (if used) will auto-reroute — no proxy config change needed
aws cloudwatch get-metric-statistics \
--namespace "AWS/RDS" \
--metric-name "AuroraBinlogReplicaLag" \
--dimensions Name=DBClusterIdentifier,Value=mcp-probe-db \
--period 60 --statistics Maximum \
--start-time $(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ)
# Execute switchover (blocks until complete or timeout)
aws rds switchover-blue-green-deployment \
--blue-green-deployment-identifier bgd-1234567890abcdef0 \
--switchover-timeout 300 # fail if lag hasn't drained within 300 seconds
If you use RDS Proxy in front of the cluster, the proxy transparently handles the endpoint swap — applications connected to the proxy endpoint experience a brief connection drop and automatic reconnect; no DNS TTL propagation or connection string change is needed. This is the primary reason to put RDS Proxy in front of any cluster that will use Blue/Green Deployments.
Pattern 4 — RDS Performance Insights: Reading Wait Events and DB Load
DBLoad is not a percentage
The most common misunderstanding of Performance Insights is treating DBLoad like CPU utilization. DBLoad measures Average Active Sessions (AAS) — the average number of database sessions that are actively executing or waiting at any given moment. A DBLoad of 1.5 means 1.5 sessions were active on average. This number only becomes alarming when it exceeds the instance's vCPU count, because once more sessions are active than there are CPUs to run them, query latency increases regardless of what the sessions are waiting on.
| DBLoad vs instance vCPUs | Interpretation | Action |
|---|---|---|
| DBLoad < 0.5 × vCPU | Lightly loaded; probe workers have write headroom | No action needed |
| DBLoad ~= vCPU | Healthy working load | Monitor; no action needed |
| DBLoad 1–2 × vCPU | Saturated; query latency increasing | Investigate Top SQL and wait events |
| DBLoad > 2 × vCPU | Severely saturated; timeouts likely | Immediate action: scale or fix query |
Enable Performance Insights on each instance — the 7-day retention tier is free for most instance classes:
aws rds modify-db-instance \
--db-instance-identifier mcp-probe-db-writer \
--enable-performance-insights \
--performance-insights-retention-period 7 \
--apply-immediately
Wait event taxonomy: the diagnostic layer
DBLoad tells you the database is overloaded. Wait events tell you why. Each wait event maps to a specific root cause and a specific fix:
| Wait event | Root cause | Fix for MCP probe workloads |
|---|---|---|
CPU | Query plans doing full table scans, complex aggregations, index rebuild | Add composite index on (server_slug, checked_at); avoid SELECT * on probe_results |
Lock:relation | Table-level lock from DDL (ALTER TABLE) competing with concurrent probe inserts | Run schema changes during maintenance window; use Blue/Green Deployments |
Lock:tuple | Row-level lock contention — multiple probe workers updating the same server_slug row | Partition probe workers by server_slug hash prefix; use INSERT ... ON CONFLICT DO UPDATE with row-level locking |
IO:DataFileRead | Buffer cache miss — hot dataset exceeds Aurora buffer pool or IOPS insufficient | Scale to larger instance class (more memory = larger buffer pool); check coverage of hot indexes |
IO:BufFileRead | Sort or hash join spilling to disk — work_mem too low for the query's sort buffer | Increase work_mem for analytics queries; avoid large aggregations in the probe write path |
Client:ClientRead | DB waiting for the application to read rows — network saturation or application-side processing delay | Add LIMIT to dashboard queries; stream result rows; check ECS task network bandwidth |
Top SQL: sort by DB load per call, not by execution count
The Top SQL tab ranks queries by their contribution to DBLoad. "Total load" (total AAS contribution) finds the highest-impact queries for overall throughput. "DB load per call" finds the slowest individual queries — a query running 100 times per minute at 500 ms average wait contributes more per invocation than a query running 10,000 times per minute at 1 ms. For MCP probe databases, the highest-impact query is almost always the bulk status insert during registry re-scans; the highest per-call load is usually an unindexed dashboard query joining across the probe_results table without a covering index:
# Via PI API: top 10 SQL statements by AAS over the past hour
aws pi get-resource-metrics \
--service-type RDS \
--identifier db-EXAMPLE123EXAMPLEABCDEFG \
--metric-queries '[{"Metric":"db.load.avg","GroupBy":{"Group":"db.sql","Dimensions":["db.sql.id","db.sql.statement"],"Limit":10}}]' \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period-in-seconds 300
For Aurora PostgreSQL, cross-reference PI's Top SQL output with pg_stat_statements for historical query statistics (total execution time, mean time per call, rows returned per call) that complement PI's real-time AAS view:
-- Enable pg_stat_statements if not already loaded
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find slowest queries by mean execution time, filtered to probe-related tables
SELECT
substring(query, 1, 120) AS query_snippet,
calls,
round((total_exec_time / 1000)::numeric, 2) AS total_sec,
round(mean_exec_time::numeric, 1) AS avg_ms,
rows / calls AS avg_rows
FROM pg_stat_statements
WHERE query ILIKE '%probe_results%' OR query ILIKE '%mcp_endpoints%'
ORDER BY mean_exec_time DESC
LIMIT 20;
Enhanced Monitoring: the OS layer alongside PI's DB layer
Performance Insights and Enhanced Monitoring are complementary, not redundant. PI sees what the database engine sees — session states, wait events, SQL plans. Enhanced Monitoring sees what the operating system sees — per-core CPU usage, memory, swap, disk IOPS, network bytes. The combination is what makes diagnosis actionable:
- CPU wait event + OS CPU spike → pure compute bottleneck: optimize queries or scale to more vCPUs.
- IO:DataFileRead wait events + OS disk IOPS spike → I/O bottleneck: increase provisioned IOPS or gp3 volume throughput, or scale instance for larger buffer pool.
- Lock:tuple + OS CPU steady → concurrency bottleneck that is not CPU-bound: reduce row lock contention by partitioning workload, not by scaling compute.
# Enable Enhanced Monitoring at 15-second OS sample granularity
aws rds modify-db-instance \
--db-instance-identifier mcp-probe-db-writer \
--monitoring-interval 15 \
--monitoring-role-arn arn:aws:iam::123456789012:role/rds-monitoring-role \
--apply-immediately
# The IAM role needs: arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole
AliveMCP's actual probe-database diagnosis
When AliveMCP scaled from 5,000 to 10,000 monitored endpoints, DBLoad during registry re-scans spiked from 0.8 (below vCPU capacity) to 3.5 (1.75× vCPU capacity on a db.r7g.large). The dominant wait event during the spike was Lock:tuple — not CPU, not IO:DataFileRead. This was the diagnostic signal that ruled out index issues and buffer pool pressure and pointed directly at row-lock contention: the bulk re-scan INSERT and the real-time probe worker were racing for the same server_slug rows.
The fix: partition probe workers by the first hex digit of the server_slug's MD5 hash (0–7 and 8–f), so the bulk re-scan worker and the real-time probe worker never target the same row range simultaneously. After deploying the partition, DBLoad during re-scans stayed below 1.8 (under vCPU capacity) and Lock:tuple disappeared from the Top Wait Events chart entirely. The fix required a schema change (adding a hash-partition column) — delivered via Blue/Green Deployment without a production outage.
Failure modes across all four patterns
| Symptom | Pattern | Cause | Fix |
|---|---|---|---|
| First query after idle takes 2–3 seconds | Serverless v2 | Cluster scaled to 0.5 ACU minimum during idle; scale-up latency on first request | Set minCapacity: 2 for production; or keep-warm cron pinging database every 10 minutes |
| "Too many connections" despite Serverless v2 scaling | Serverless v2 | Lambda burst saturates max_connections before ACU scale-up responds | Add RDS Proxy — multiplexes thousands of Lambda connections through a fixed native pool |
| Data API error after v1 → v2 migration | Serverless v2 | Data API not available on Aurora Serverless v2 | Migrate callers to RDS Proxy + @aws-sdk/rds-signer IAM auth pattern |
| Managed failover stuck "failing-over" for >5 minutes | Global Database | Long-running open transactions on primary prevent lag from draining to zero | Set statement_timeout on background workers; terminate transactions before initiating failover |
| Write forwarding returns "cannot execute insert in read-only transaction" | Global Database | Write forwarding not enabled on the secondary cluster, or not enabled in the session | Enable via modify-db-cluster --enable-global-write-forwarding; verify with SHOW aurora_read_replica_read_consistency |
| Blue/Green deployment stuck in PROVISIONING for hours | Blue/Green | Large database (500 GB+) requires multi-hour initial snapshot restore; heavy write load on blue slows replication catch-up | Wait; avoid creating deployments during bulk imports on blue |
| "logical replication is not enabled" on deployment creation | Blue/Green | PostgreSQL rds.logical_replication = 1 not set, or set but not yet applied (reboot pending) | Update parameter group, apply immediately, reboot the source cluster |
| Application throws "column does not exist" after switchover | Blue/Green | Schema on green dropped a column; application code rolled back to old blue schema after rollback | Use expand-contract pattern: add new column before removing old one; never drop until all code is migrated |
| Performance Insights tab greyed out in console | Performance Insights | Instance class does not support PI (db.t2 family) | Upgrade instance to db.t3.medium or larger; or use Aurora Serverless v2 which always supports PI |
| Top SQL shows "SQL not available" for all queries | Performance Insights | RDS MySQL with performance_schema disabled | Enable performance_schema = 1 in parameter group and reboot |
| DBLoad above vCPU but no single slow query visible in Top SQL | Performance Insights | Load distributed across many short queries; no single query dominates | Look at wait event type — if Lock:tuple, reduce concurrency via RDS Proxy or workload partitioning |
Sequencing these four patterns in a real MCP backend rollout
These four patterns are not independent — they have dependencies that determine the right sequence to deploy them:
- Deploy Serverless v2 first, with RDS Proxy in front. This establishes the connection pooling layer that Blue/Green Deployments and failover patterns depend on — RDS Proxy transparently handles endpoint swaps during both Blue/Green switchovers and Global Database failovers.
- Enable Performance Insights during initial deployment, not after a performance problem appears. PI data from the first weeks of production load becomes the baseline that makes future anomalies detectable.
- Add Global Database after the primary cluster is stable, not during initial provisioning. The first 2–4 weeks of production usage will reveal the real write patterns — knowing the actual write throughput helps right-size the secondary clusters'
minCapacity(secondary clusters that serve read-only status dashboards need far less capacity than the writer). - Use Blue/Green Deployments for the first schema migration that arrives after the cluster is live. Setting up logical replication on an already-live cluster requires a reboot — schedule that reboot during the initial quieter phase of production traffic rather than under production load pressure.