Guide · AWS RDS · Database Observability
RDS Performance Insights for MCP Servers — DB Load, Wait Events, Top SQL Analysis
RDS Performance Insights is a built-in query-level observability layer for Aurora and RDS that exposes database load as Average Active Sessions (AAS) decomposed by wait event, SQL query, host, and user — MCP server teams use it to identify which probe queries are saturating the database during registry scans, why alert fanout causes lock contention, and which indexes are missing on high-frequency uptime tables. Three Performance Insights concepts are misunderstood most often: DBLoad is unitless, not a percentage — a value of 4 means 4 sessions are concurrently active (waiting or executing), and this number only becomes alarming when it exceeds the number of vCPUs on the instance (a db.r7g.large has 2 vCPUs, so DBLoad > 2 means the database is CPU-saturated); wait events reveal the bottleneck category, not just that the database is slow — a workload dominated by io/file/innodb/innodb_data_file needs more IOPS provisioned or a buffer pool that fits the hot dataset, while a workload dominated by lock/table/sql/handler_table_lock needs query restructuring to reduce lock contention; and Top SQL by AAS is more actionable than Top SQL by execution count — a query that runs 10,000 times per minute but each execution takes 1 ms contributes less to overall load than a query running 100 times per minute with 500 ms average wait time.
TL;DR
Enable Performance Insights on your RDS/Aurora cluster (7-day free retention). Open the PI dashboard and look at the DB Load chart. If DBLoad exceeds your vCPU count, the database is saturated — decompose by wait event to find whether it is CPU, I/O, or lock contention. Use the Top SQL tab to find the highest-AAS queries and add the appropriate indexes. Enable Enhanced Monitoring separately to see OS-level CPU and memory alongside PI.
Enabling Performance Insights
# Enable Performance Insights on an Aurora cluster (applied to each instance)
aws rds modify-db-instance \
--db-instance-identifier mcp-probe-db-writer \
--enable-performance-insights \
--performance-insights-retention-period 7 \ # 7 days = free; 93/185/372 days = paid
--apply-immediately
# For new clusters via CDK:
const writer = rds.ClusterInstance.serverlessV2("writer", {
performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT, // 7 days, free
// or: rds.PerformanceInsightRetention.MONTHS_1 (31 days, $0.02/vCPU-hr)
enablePerformanceInsights: true,
// Optional: encrypt PI data with a customer-managed KMS key
// performanceInsightEncryptionKey: myKey,
});
Supported instance classes: Performance Insights requires db.t3.medium or larger for RDS, and any Serverless v2 instance for Aurora. It is not available on db.t2 instances or Aurora Serverless v1. If your instance class is incompatible, Performance Insights is silently disabled with no error message — verify in the console that the PI tab appears.
Understanding DB Load and AAS
Average Active Sessions (AAS) counts the number of database sessions that are in one of two states: executing (actively using CPU) or waiting (blocked on I/O, a lock, or a network roundtrip). A session that is idle (connected but not executing a query) does not contribute to AAS.
| DBLoad value | vs 2-vCPU instance (db.r7g.large) | Interpretation |
|---|---|---|
| 0 – 0.5 | Below capacity | Database is mostly idle; probe workers have write headroom |
| 0.5 – 2.0 | At or near capacity | Normal healthy load for a moderate MCP probe fleet |
| 2 – 4 | Saturated (1–2× vCPU) | Database is overloaded; query latency will increase; investigate Top SQL |
| >4 | Severely saturated (>2× vCPU) | Timeouts likely; immediate action required — scale up instance or fix query |
# Query Performance Insights API for recent DBLoad
aws pi get-resource-metrics \
--service-type RDS \
--identifier db-EXAMPLE123EXAMPLEABCDEFG \
--metric-queries '[{"Metric":"db.load.avg","GroupBy":{"Group":"db.wait_event","Dimensions":["db.wait_event.name","db.wait_event.type"],"Limit":5}}]' \
--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 60
Wait event taxonomy for Aurora PostgreSQL
Performance Insights groups wait events by type. Each type points to a different root cause:
| Wait event | Type | Root cause | Fix |
|---|---|---|---|
CPU | CPU | Pure compute — complex query plans, index scans on large tables, aggregations | Add indexes; rewrite queries to avoid full table scans; consider read replicas for analytics |
Lock:relation | Lock | Table-level lock contention — DDL statements (ALTER TABLE) competing with OLTP queries | Run DDL during low-traffic windows; use pg_try_advisory_lock for non-blocking migrations; avoid long-running transactions |
Lock:tuple | Lock | Row-level lock contention — UPDATE/DELETE on the same rows from concurrent probe workers | Use SELECT FOR UPDATE SKIP LOCKED for queue-style processing; partition probe workers by server_slug prefix to reduce row contention |
IO:BufFileRead | I/O | Spill to disk during sort or hash join — sort_mem / work_mem too low for the query's data volume | Increase work_mem for the specific query or session; avoid large aggregations in the probe write path |
IO:DataFileRead | I/O | Buffer cache miss — data not in memory; either hot dataset exceeds buffer pool or I/O throughput is insufficient | Scale up instance (more memory = larger buffer pool); increase provisioned IOPS; add a covering index to reduce data scanned |
Client:ClientRead | Client | Database is waiting for the client to consume result rows — network saturation or slow application processing | Use LIMIT on large result sets; stream results rather than loading all rows into memory; check application-side network bandwidth |
Timeout:PgSleep | Timeout | Intentional sleep in a query or stored procedure; usually a polling anti-pattern | Replace pg_sleep polling loops with LISTEN/NOTIFY or SQS-based event dispatch |
Top SQL analysis
The Top SQL tab in Performance Insights ranks queries by their contribution to DBLoad (AAS). Sort by "DB load per call" to find the slowest individual queries, or by "Total load" to find the queries contributing most to overall saturation:
# Via PI API: get top 10 SQL statements by DB load 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.db_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
Performance Insights captures query text automatically for Aurora PostgreSQL (via pg_stat_activity) and Aurora MySQL (via performance_schema). For RDS MySQL, you must enable performance_schema = 1 in the parameter group — if it is disabled, PI captures query digest hashes but not the full query text, making Top SQL less useful.
-- In Aurora PostgreSQL, cross-reference PI findings with pg_stat_statements
-- (extension must be enabled):
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find the slowest queries by total execution time
SELECT
substring(query, 1, 100) AS query_snippet,
calls,
total_exec_time / 1000 AS total_sec,
mean_exec_time AS avg_ms,
stddev_exec_time AS stddev_ms,
rows / calls AS avg_rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
Enhanced Monitoring vs Performance Insights
These two observability features are complementary and capture different layers:
| Feature | What it observes | Granularity | Storage |
|---|---|---|---|
| Enhanced Monitoring | OS-level metrics: CPU per core, memory, swap, disk IOPS, network bytes | 1–60 seconds (configurable) | CloudWatch Logs (/aws/rds/instance/{id}/osMetrics) |
| Performance Insights | DB-level metrics: AAS, wait events, Top SQL, top hosts, top users | 1 second (in-dashboard); 1-minute API granularity | PI service storage (7 days free, paid tiers available) |
# Enable Enhanced Monitoring alongside Performance Insights
aws rds modify-db-instance \
--db-instance-identifier mcp-probe-db-writer \
--monitoring-interval 15 \ # seconds between OS metric samples
--monitoring-role-arn arn:aws:iam::123456789012:role/rds-monitoring-role \
--apply-immediately
# The IAM role must have AmazonRDSEnhancedMonitoringRole policy attached
When diagnosing a DBLoad spike: use PI to confirm which queries and wait events are contributing; use Enhanced Monitoring to confirm whether the OS CPU is also spiked (CPU wait event + OS CPU spike = compute bottleneck) or whether the OS shows high disk I/O while DB shows IO:DataFileRead (I/O bottleneck, not compute).
Counter metrics for I/O analysis
Performance Insights exposes engine-specific counter metrics alongside AAS. For Aurora PostgreSQL these include:
# Useful PI counter metrics for probe database analysis
# db.SQL.tup_fetched.avg — rows fetched per second (high = large result sets or full scans)
# db.SQL.tup_inserted.avg — rows inserted per second (probe write throughput)
# db.Checkpoint.buffers_checkpoint.avg — buffers written at checkpoint (high = dirty buffer pressure)
# db.Cache.blks_hit.avg — buffer cache hits (high = good; declining = working set exceeding buffer pool)
# db.Cache.blks_read.avg — disk reads per second (high relative to blks_hit = cache miss rate problem)
aws pi get-resource-metrics \
--service-type RDS \
--identifier db-EXAMPLE123EXAMPLEABCDEFG \
--metric-queries \
'[{"Metric":"db.SQL.tup_inserted.avg"},{"Metric":"db.Cache.blks_hit.avg"},{"Metric":"db.Cache.blks_read.avg"}]' \
--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 60
AliveMCP probe-database observability pattern
AliveMCP enables Performance Insights with DEFAULT retention (7 days) on the Aurora writer instance. During a registry re-scan event (once every 6 hours, bulk-inserts ~12,000 status rows), DBLoad spikes from a baseline of 0.8 to approximately 3.5 on a db.r7g.large (2 vCPUs). The dominant wait event during this spike is Lock:tuple — the bulk insert contends for row locks with the real-time probe worker that is concurrently updating the same server_slug rows. The fix applied: partition probe workers by server_slug hash prefix (0–7 and 8–f), so the bulk re-scan and the real-time probe worker never compete for the same row range. Post-fix, DBLoad during re-scans stays below 2 (within vCPU capacity) and Lock:tuple disappears from the Top Wait Events chart. The PI data was exported via the get-resource-metrics API into a CloudWatch custom dashboard alongside the ServerlessDatabaseCapacity ACU metric for a single-pane-of-glass database health view.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Performance Insights tab is greyed out or missing in console | Instance class does not support PI (db.t2 family), or PI was not enabled when the instance was created | Modify the instance to enable PI; for db.t2 instances, upgrade to db.t3.medium or larger first |
| Top SQL shows "SQL not available" for all queries | For RDS MySQL, performance_schema is not enabled in the parameter group; PI captures load but cannot associate it with query text | Create a parameter group with performance_schema = 1, apply it to the instance, and reboot |
| DBLoad is always 0.0 despite visible query traffic | PI is enabled but the instance is collecting metrics; can take up to 5 minutes after enabling to show data. Or: the database is so lightly loaded that AAS is consistently below 0.1 and rounds to 0 in the chart scale | Wait 5 minutes after enabling; zoom the chart Y-axis to see sub-1 AAS values; run a synthetic load test to verify PI is collecting |
| DBLoad consistently exceeds vCPU count but Top SQL shows only fast queries | The load is distributed across hundreds of short queries rather than a single slow one; no single query dominates Top SQL | Look at wait event breakdown — if dominated by Lock:tuple or Lock:relation, the issue is concurrency, not individual query latency; reduce connection count via RDS Proxy |
| PI data retention gap after instance modification | Changing the instance class restarts the instance; PI data collected before the restart is still available but the timeline has a gap | Expected behavior; access pre-modification data by selecting the time range before the modification in the PI console |
| PI API returns "InvalidParameterCombination" for counter metrics | Counter metric names are engine-specific; Aurora PostgreSQL counters differ from RDS MySQL counters; using a MySQL metric name against a PostgreSQL instance fails | Use list-available-resource-metrics to enumerate valid metric names for your specific DB engine and version |