Skip to content

RDS high CPU: finding the query, not just resizing the instance

RDS CPU pinned at 90-100% is almost always a handful of queries, not undersized hardware. Here is how to find them with Performance Insights and fix the cause before paying for a bigger instance.

An RDS instance pinned at 90-100% CPU is almost never “the instance is too small” — it is usually two or three queries doing far more work than they should, multiplied by request volume. Open Performance Insights, sort by CPU, and the offenders are on the first screen. Resizing the instance without doing that step works, costs real money every month, and lasts exactly until traffic grows into the new headroom.

What’s happening

Database CPU goes to a small number of things: evaluating rows (full table scans, large joins, sorting), parsing and planning, and — on burstable classes — sometimes nothing at all, because the real story is CPU credits. Each is visible if you look in the right place.

The pattern we see most: a query that was fine at 100k rows becomes a scan-and-sort at 10M rows. It still returns in 300 ms so nobody notices, but it burns a full core per execution, and at 50 executions per second the instance saturates. Second most common: an ORM emitting N+1 query storms — thousands of cheap queries whose per-statement parse and network overhead adds up to a pinned CPU with no single “slow query” to find. Third: a missing index after a schema change or a new feature, which turns point lookups into scans.

The burstable-class trap deserves its own paragraph. On db.t3 and db.t4g instances, sustained CPU above the baseline spends credits. When CPUCreditBalance hits zero (in standard mode), the instance is throttled to its baseline — the workload did not change, but everything got slower, and CPU shows high because the ceiling dropped. If you run production on a t-class, check the credit metrics before anything else.

What to check first

  • CPUUtilization shape in RDS > Databases > your-instance > Monitoring — a step change points at a deploy or a data-size threshold crossed; a daily ramp points at traffic; a plateau at 100% after a slow climb on a t-class points at credits.
  • CPUCreditBalance if the class starts with db.t — zero balance is your answer.
  • Whether Performance Insights is on. It is enabled by default on most modern instance creation paths and the free tier retains 7 days; if it is off, turn it on now — you will need it for every future incident too.
  • What changed — deploys, migrations, a marketing send, a batch job schedule.
  • Enhanced Monitoring to confirm CPU is going to the engine process and not, say, to an OS-level burst of swap activity.

How to verify

Performance Insights is the primary tool. Console: RDS > Performance insights, pick the instance, set the time range to the incident, and slice the load by Top SQL. The database load chart (average active sessions) split by wait state tells you whether load is CPU or waiting on I/O and locks — those are different articles.

From the CLI, the same data:

aws pi get-resource-metrics \
  --service-type RDS \
  --identifier db-ABCDEFGHIJKL123 \
  --metric-queries '[{"Metric":"db.load.avg","GroupBy":{"Group":"db.sql_tokenized","Limit":5}}]' \
  --start-time 2026-07-04T08:00:00Z --end-time 2026-07-04T10:00:00Z \
  --period-in-seconds 300

Inside the engine, confirm what the top statements are doing:

-- Postgres: top statements by total time (requires pg_stat_statements)
SELECT calls, ROUND(total_exec_time::numeric/1000,1) AS total_s,
       ROUND(mean_exec_time::numeric,2) AS mean_ms, LEFT(query,80)
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

-- MySQL: top statements from performance_schema
SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1e12 AS total_s
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

Then run EXPLAIN (Postgres: EXPLAIN (ANALYZE, BUFFERS)) on the top statement and look for sequential scans and sorts over large row counts.

The fix

  1. Identify the top two or three statements by load. In every real incident we have worked, the top three statements account for 60-90% of DB load. Do not tune the long tail.
  2. Fix the access path. Add the missing index, rewrite the query to use an existing one, or push filtering into the database instead of fetching and discarding. One index has, more than once in our experience, taken an instance from 95% CPU to 20%.
  3. Kill the N+1. If the top statement is trivial but executes tens of thousands of times per minute, the fix is in the application: eager-load the association or batch the lookups. No index helps here.
  4. Cache what the database keeps recomputing. A leaderboard, a settings blob, a dashboard aggregate hit on every request belongs in ElastiCache or application memory with a short TTL.
  5. Move reads to a replica if load is genuinely read-volume and already efficient — see read replicas in a multi-tenant SaaS for the lag caveats before you route traffic there.
  6. Then, and only then, resize. If the top queries are already efficient, the workload is real, and load is still high, scale the instance class — or move a t-class to unlimited mode or an m/r-class if credits were the story. This is the correct last step, not the reflexive first one.

When this is the wrong move

If Performance Insights shows load dominated by I/O waits (IO:DataFileRead, io/table/sql/handler) rather than CPU, the bottleneck is storage, not compute — a bigger instance mostly buys more page cache, and the honest fix is indexes or IOPS. If CPU is high but flat at 40-60% and latency is fine, you may not have a problem at all; databases are allowed to work. And during a genuine traffic surge with efficient queries, resizing is the right move — this article is against unexamined resizing, not against capacity. The Amazon RDS documentation covers Performance Insights retention tiers if you need more than the free window.

Common mistakes

  • Resizing first, concluding “that fixed it”, and never finding the query that will pin the bigger instance in six months.
  • Ignoring CPUCreditBalance on t-class instances and debugging a throttle as if it were load.
  • Tuning by mean_exec_time alone — a 2 ms query called 100k times/minute outweighs a 4-second report that runs hourly.
  • Restarting the instance to “clear” CPU: you drop the buffer pool/cache, and the warm-up makes the next hour worse.

Prevention

  • Alarm on CPU p95 over 80% sustained 15 minutes, not on instantaneous spikes.
  • Keep Performance Insights enabled everywhere; seven days of free retention has solved most incidents for us.
  • Review top SQL monthly — queries degrade as data grows, and the review catches them at 40% CPU instead of 100%.
  • Run EXPLAIN on new queries touching large tables as part of code review.
  • Keep production off burstable classes once baseline CPU exceeds roughly the class baseline; the credit math stops working in your favour.

High CPU is the database telling you exactly which query to fix, through Performance Insights, for free — resizing is paying monthly to not listen. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.