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 valuevs 2-vCPU instance (db.r7g.large)Interpretation
0 – 0.5Below capacityDatabase is mostly idle; probe workers have write headroom
0.5 – 2.0At or near capacityNormal healthy load for a moderate MCP probe fleet
2 – 4Saturated (1–2× vCPU)Database is overloaded; query latency will increase; investigate Top SQL
>4Severely 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 eventTypeRoot causeFix
CPUCPUPure compute — complex query plans, index scans on large tables, aggregationsAdd indexes; rewrite queries to avoid full table scans; consider read replicas for analytics
Lock:relationLockTable-level lock contention — DDL statements (ALTER TABLE) competing with OLTP queriesRun DDL during low-traffic windows; use pg_try_advisory_lock for non-blocking migrations; avoid long-running transactions
Lock:tupleLockRow-level lock contention — UPDATE/DELETE on the same rows from concurrent probe workersUse SELECT FOR UPDATE SKIP LOCKED for queue-style processing; partition probe workers by server_slug prefix to reduce row contention
IO:BufFileReadI/OSpill to disk during sort or hash join — sort_mem / work_mem too low for the query's data volumeIncrease work_mem for the specific query or session; avoid large aggregations in the probe write path
IO:DataFileReadI/OBuffer cache miss — data not in memory; either hot dataset exceeds buffer pool or I/O throughput is insufficientScale up instance (more memory = larger buffer pool); increase provisioned IOPS; add a covering index to reduce data scanned
Client:ClientReadClientDatabase is waiting for the client to consume result rows — network saturation or slow application processingUse LIMIT on large result sets; stream results rather than loading all rows into memory; check application-side network bandwidth
Timeout:PgSleepTimeoutIntentional sleep in a query or stored procedure; usually a polling anti-patternReplace 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:

FeatureWhat it observesGranularityStorage
Enhanced MonitoringOS-level metrics: CPU per core, memory, swap, disk IOPS, network bytes1–60 seconds (configurable)CloudWatch Logs (/aws/rds/instance/{id}/osMetrics)
Performance InsightsDB-level metrics: AAS, wait events, Top SQL, top hosts, top users1 second (in-dashboard); 1-minute API granularityPI 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

SymptomCauseFix
Performance Insights tab is greyed out or missing in consoleInstance class does not support PI (db.t2 family), or PI was not enabled when the instance was createdModify 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 queriesFor RDS MySQL, performance_schema is not enabled in the parameter group; PI captures load but cannot associate it with query textCreate a parameter group with performance_schema = 1, apply it to the instance, and reboot
DBLoad is always 0.0 despite visible query trafficPI 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 scaleWait 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 queriesThe load is distributed across hundreds of short queries rather than a single slow one; no single query dominates Top SQLLook 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 modificationChanging the instance class restarts the instance; PI data collected before the restart is still available but the timeline has a gapExpected behavior; access pre-modification data by selecting the time range before the modification in the PI console
PI API returns "InvalidParameterCombination" for counter metricsCounter metric names are engine-specific; Aurora PostgreSQL counters differ from RDS MySQL counters; using a MySQL metric name against a PostgreSQL instance failsUse list-available-resource-metrics to enumerate valid metric names for your specific DB engine and version