Database CPU Suddenly Reaches 90%: What Do You Check First?
A practical production debugging guide for investigating sudden database CPU spikes, finding expensive queries, reading execution plans, checking indexes, connections, traffic, and recent changes.
17 min read
Database CPU at 90%? Find the Workload Before Scaling
Your database CPU suddenly jumps from normal usage to 90%.
API latency increases.
Requests begin timing out.
Production alerts start firing.
The wrong reaction is to immediately restart the database, increase the instance size, or add random indexes.
The right question is:
What changed, and which workload is making the database work this hard?
This guide walks through a practical production debugging process for finding the root cause before making infrastructure changes.
The First Thing to Check
When database CPU suddenly reaches 90%, start by identifying the queries creating the most workload.
You want to answer three questions:
- Which queries are consuming the most resources?
- Are they expensive individually or simply running too frequently?
- What changed around the time the CPU increased?
A useful investigation flow is:
CPU Spike
|
v
Confirm the timeline
|
v
Find expensive queries
|
v
Inspect active queries
|
v
Check execution plans
|
v
Check indexes and scans
|
v
Correlate traffic and connections
|
v
Check deployments and jobs
|
v
Fix the root causeWhy Database CPU Can Suddenly Increase
High CPU can come from many different sources:
- A query starts performing a full table scan
- A missing index causes millions of rows to be examined
- An application deployment introduces an inefficient query
- Traffic increases sharply
- An ORM introduces an N+1 query pattern
- A scheduled job starts processing large amounts of data
- Application instances create too many concurrent database connections
- Sorting or aggregation becomes expensive
- A query execution plan changes
- A large join starts processing far more rows than expected
All of these can produce the same metric:
CPU = 90%
That is why CPU utilization tells you where the pressure is visible, but not what created it.
Step 1: Confirm When the Spike Started
Start with monitoring.
Depending on your infrastructure, this could be:
- Prometheus + Grafana
- AWS RDS Performance Insights
- CloudWatch
- Datadog
- New Relic
- Azure Monitor
- Google Cloud Monitoring
Check:
- Database CPU
- Query latency
- Requests per second
- Queries per second
- Database connections
- Disk I/O
- Memory pressure
The timeline can immediately narrow the investigation.
For example:
10:00 CPU 25%
10:05 Deployment completed
10:07 CPU 42%
10:10 CPU 75%
10:12 CPU 92%A recent deployment becomes suspicious.
Compare that with:
01:55 CPU 20%
02:00 Reporting job starts
02:02 CPU 88%
02:15 Reporting job finishes
02:16 CPU 22%Now the scheduled reporting workload is a much stronger candidate.
Before touching the database configuration, determine exactly when the behavior changed.
Step 2: Find the Queries Creating the Most Work
For PostgreSQL, pg_stat_statements is one of the most useful tools for understanding query workload.
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;You can inspect queries by total execution time:
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;This helps you identify:
- Frequently executed queries
- Individually slow queries
- Queries consuming large amounts of total execution time
- Unexpected queries generating heavy workload
Do not look only for the slowest query.
There are two common failure patterns.
One Expensive Query
Calls: 20
Average time: 8 secondsThe query itself may be inefficient.
One Fast Query Running Too Often
Calls: 3,000,000
Average time: 5 msFive milliseconds looks harmless.
Three million executions are not.
The better mental model is:
Query Cost
x
Execution Frequency
=
Total Database WorkA production incident can be caused by either side of that equation.
Step 3: Check What Is Running Right Now
Historical statistics show what has happened.
During an active incident, also check what is currently executing.
In PostgreSQL:
SELECT
pid,
state,
query_start,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;Look for:
- Long-running queries
- Many copies of the same query
- Unexpected batch operations
- Reporting queries
- Queries waiting on resources
- Large concurrent workloads
For example, imagine seeing 150 copies of:
SELECT *
FROM products
WHERE LOWER(name) LIKE '%oil%';The problem may not be one extremely slow request.
It may be hundreds of expensive searches hitting the database at the same time.
Step 4: Inspect the Execution Plan
Once you identify a suspicious query, understand how the database executes it.
For example:
EXPLAIN ANALYZE
SELECT id, customer_id, created_at
FROM orders
WHERE customer_id = 98231
ORDER BY created_at DESC
LIMIT 50;Suppose the result shows:
Seq Scan on orders
Rows Removed by Filter: 18,500,000
Execution Time: 2400 msThe database is examining millions of rows to return only a small result set.
A better plan might look more like:
Index Scan using idx_orders_customer_created
Execution Time: 4 msThe execution plan tells you whether the database is spending time on things such as:
- Sequential scans
- Large row scans
- Expensive sorting
- Expensive joins
- Poor filtering
- Incorrect row estimates
Step 5: Check Whether the Query Has the Right Index
Suppose this query runs frequently:
SELECT id, customer_id, created_at
FROM orders
WHERE customer_id = 98231
ORDER BY created_at DESC
LIMIT 50;A useful index may be:
CREATE INDEX idx_orders_customer_created
ON orders (customer_id, created_at DESC);Without an appropriate index, the database may need to:
Large orders table
|
v
Scan many rows
|
v
Filter customer_id
|
v
Sort matches
|
v
Return 50With a suitable index:
Index lookup
|
v
Matching customer rows
|
v
Already in useful order
|
v
Return 50But do not treat indexes as a universal solution.
Indexes also have costs:
- Additional disk usage
- More memory usage
- Slower inserts
- Slower updates
- Slower deletes
- Additional maintenance work
The goal is not to create more indexes.
The goal is to create indexes that match important query access patterns.
Step 6: Investigate Large Sequential Scans
Sequential scans are not automatically bad.
For a small table, scanning the whole table may be faster than using an index.
They become suspicious when large tables are repeatedly scanned for queries that return only a small subset of data.
In PostgreSQL:
SELECT
relname,
seq_scan,
seq_tup_read,
idx_scan
FROM pg_stat_user_tables
ORDER BY seq_tup_read DESC;Imagine a table with:
Table: orders
Rows: 80,000,000
Sequential scans: very high
Index scans: very lowThat is not proof of a problem by itself.
But it tells you that queries using this table deserve closer inspection.
Always connect table-level statistics back to actual queries and their execution plans.
Step 7: Determine Whether Traffic Increased
Sometimes the queries are fine.
The workload simply became much larger.
Suppose your normal database load is:
API requests: 1,000 / second
DB queries: 3,000 / second
CPU: 30%Then traffic suddenly becomes:
API requests: 5,000 / second
DB queries: 15,000 / second
CPU: 90%That is very different from a CPU spike caused by one broken query.
Check:
- Requests per second
- Queries per second
- Concurrent users
- Endpoint traffic
- Read/write ratio
- Cache hit rate
If traffic increased significantly and your queries remain efficient, the issue may genuinely be capacity.
Possible solutions then include:
- Better caching
- Read replicas
- Reducing unnecessary queries
- Connection control
- Vertical scaling
The important point is that you reach this conclusion from measurements rather than assuming it immediately.
Step 8: Check Database Connections and Concurrency
Application scaling can create unexpected pressure on the database.
Imagine:
10 API instances
|
| 100 connections each
v
1,000 possible DB connectionsIf autoscaling increases the application fleet:
50 API instances
|
| 100 connections each
v
5,000 possible DB connectionsThe application tier may scale successfully while overwhelming the database.
In PostgreSQL:
SELECT
state,
COUNT(*)
FROM pg_stat_activity
GROUP BY state;You may see something like:
active: 250
idle: 700
idle in transaction: 40Pay particular attention to unexpected connection growth and idle in transaction sessions.
Connection count is not always the direct cause of high CPU, but excessive concurrency can amplify an already expensive workload.
Step 9: Look for N+1 Queries
Application code can accidentally generate far more database work than expected.
Suppose one request loads 100 users:
SELECT *
FROM users
LIMIT 100;Then separately loads orders for each user:
SELECT *
FROM orders
WHERE user_id = 1;
SELECT *
FROM orders
WHERE user_id = 2;
SELECT *
FROM orders
WHERE user_id = 3;Continue that pattern for all 100 users.
One API request becomes:
1 users query
+
100 orders queries
=
101 database queriesIf 500 requests arrive:
500 API requests
|
v
50,500 database queriesThis commonly appears with:
- ORMs
- GraphQL resolvers
- Lazy-loaded relationships
- Nested API responses
Possible fixes include:
- Joins
- Batch queries
- Eager loading
- DataLoader
- Query restructuring
The important lesson is that a database CPU problem may originate in application behavior.
Step 10: Check Recent Deployments and Query Changes
When performance suddenly changes, compare it with recent application changes.
Check:
- Deployments
- Database migrations
- New queries
- ORM changes
- Configuration changes
- New features
- Index changes
For example, suppose this query previously used an index on email:
SELECT *
FROM users
WHERE email = $1;It gets changed to:
SELECT *
FROM users
WHERE LOWER(email) = LOWER($1);Depending on the database and available indexes, the new expression may no longer use the previous index efficiently.
The application may still function correctly.
The performance characteristics changed.
That is why deployment timestamps should always be visible alongside production metrics.
Step 11: Check Background Jobs
Not every database query comes from an API request.
Background workloads can include:
- Cron jobs
- Report generation
- Queue workers
- Data cleanup
- Analytics aggregation
- Search indexing
- Data synchronization
- ETL processes
- Invoice generation
Consider:
UPDATE orders
SET archived = true
WHERE created_at < NOW() - INTERVAL '2 years';If millions of rows qualify, this job can generate a significant workload.
You may see:
00:00 CPU 22%
01:00 CPU 20%
02:00 CPU 91% <-- background job starts
02:30 CPU 89%
03:00 CPU 24%Repeated spikes at predictable times are a strong signal to inspect scheduled jobs.
Step 12: Watch Expensive Sorting and Aggregation
Queries involving large datasets can become CPU-heavy when they perform operations such as:
ORDER BYGROUP BYDISTINCTCOUNTSUMAVG- Window functions
- Large joins
For example:
SELECT
customer_id,
COUNT(*) AS total_orders
FROM orders
GROUP BY customer_id
ORDER BY total_orders DESC;Running this over millions of rows for every user request can become expensive.
The best solution may not be another index.
Depending on the workload, you may need:
- Cached results
- Precomputed values
- Materialized views
- Background aggregation
- Read models
- A separate analytics workload
This is where query optimization starts connecting with system design.
Step 13: Consider Query Plan Regressions
Sometimes the SQL has not changed at all.
The execution plan has.
Database optimizers use statistics and cost estimates to decide how to execute a query.
As the data grows or changes, the optimizer may choose a different plan.
For example:
Before
Index Scan
Execution Time: 15 msLater:
Sequential Scan
Execution Time: 2,500 msPossible reasons include:
- Table growth
- Changed data distribution
- Stale statistics
- Reduced index selectivity
- Different query parameters
- Configuration changes
If a previously fast query suddenly becomes expensive, compare its execution plan with earlier behavior.
Make Sure CPU Is Actually the Bottleneck
A slow database does not automatically mean CPU is the primary problem.
Compare CPU with:
- Disk latency
- IOPS
- Memory pressure
- Cache hit ratio
- Network latency
- Query waits
- Lock waits
For example:
CPU: 35%
Disk utilization: 100%
Disk latency: very highThe primary bottleneck is unlikely to be CPU.
Compare that with:
CPU: 95%
Disk: normal
Memory: normal
Expensive queries: highThat is much more consistent with a CPU-bound workload.
Production debugging should always focus on the resource that is actually saturated.
What About Locks?
Lock contention can make database requests extremely slow without necessarily creating high CPU.
For example:
Transaction A
|
| holds lock
v
Row
Transaction B ---- waits
Transaction C ---- waits
Transaction D ---- waitsThe waiting transactions increase application latency, but they may not consume much CPU while waiting.
PostgreSQL exposes useful fields in pg_stat_activity:
wait_event_type
wait_eventThese help separate actively executing queries from queries blocked on another resource.
A Realistic Production Investigation
Suppose Grafana reports:
PostgreSQL CPU > 90%Your monitoring shows:
CPU 92%
Traffic normal
Connections normal
Disk I/O normal
Latency increasedTraffic did not increase.
That points you toward the query workload.
You inspect pg_stat_statements and find this query consuming a large amount of execution time:
SELECT id, status, created_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 20;You inspect its execution plan:
EXPLAIN ANALYZE
SELECT id, status, created_at
FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;The plan shows:
Sequential Scan
Millions of rows examined
Large sortThe table currently has:
PRIMARY KEY (id)
INDEX (user_id)But the access pattern repeatedly filters by user_id and sorts by created_at.
A more suitable index may be:
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);After safely applying and verifying the change:
Before
Execution Time: 1,800 ms
CPU: 92%After
Execution Time: 6 ms
CPU: 34%The important part is not that adding an index always fixes high CPU.
It is that the fix came from evidence:
High CPU
|
v
Expensive query identified
|
v
Execution plan inspected
|
v
Inefficient access pattern found
|
v
Targeted fix applied
|
v
Metrics verifiedWhen Should You Scale the Database?
Scaling is valid when the workload is healthy but the database is reaching its capacity.
For example:
Traffic: 5x normal
Queries: optimized
Database throughput: near limit
CPU: 90%Now infrastructure changes may be appropriate.
Possible options include:
- Vertical scaling
- Read replicas
- Better caching
- Moving analytical workloads away from the primary database
- Reducing unnecessary reads
- Sharding at much larger scale
Scaling becomes wasteful when it is used to hide inefficient queries.
If one broken query scans millions of unnecessary rows, doubling the CPU may only postpone the next incident.
Common Mistakes
Restarting the Database Immediately
A restart may temporarily reduce CPU, but the same workload can return immediately.
It can also remove useful evidence about the incident.
Adding Indexes Without Inspecting the Query Plan
An index should solve a specific access pattern.
Random indexes increase storage and write overhead.
Looking Only at Slow Queries
A query does not need to be individually slow to create a large workload.
Frequency matters just as much as latency.
Assuming Every 90% CPU Event Requires Bigger Hardware
High CPU can be caused by application behavior, query design, concurrency, scheduled jobs, or genuine traffic growth.
Determine which one you have first.
Ignoring Application Metrics
Database metrics become much more useful when correlated with:
- API traffic
- Deployments
- Queue activity
- Scheduled jobs
- Connection count
- Endpoint latency
- Error rates
Running EXPLAIN ANALYZE Carelessly
Remember that EXPLAIN ANALYZE actually executes the query.
Use caution with expensive queries and modifying statements in production.
Optimizing Without Measuring
Always compare before and after.
Useful measurements include:
- Execution time
- Rows scanned
- Query frequency
- CPU utilization
- Application latency
Production Debugging Checklist
When database CPU suddenly reaches 90%, work through this sequence:
1. Confirm the timeline
Check CPU, latency, traffic, connections, memory, and disk metrics.
2. Find the top queries
Look at total execution time, average execution time, and call frequency.
3. Check active queries
See what is executing during the incident.
4. Inspect execution plans
Look for unnecessary scans, joins, sorting, and poor estimates.
5. Verify indexes
Make sure important access patterns are supported by appropriate indexes.
6. Correlate application workload
Check traffic, N+1 queries, endpoint behavior, and connection growth.
7. Review recent changes
Compare the spike with deployments, migrations, and configuration changes.
8. Inspect background jobs
Check scheduled reports, workers, analytics, and data-processing jobs.
9. Verify the actual bottleneck
Make sure the database is CPU-bound rather than waiting on disk, locks, memory, or another resource.
10. Apply the smallest appropriate fix
That may be query optimization, indexing, caching, workload control, read replicas, or database scaling.
Best Practices
- Enable query observability before incidents happen
- Track both slow queries and high-frequency queries
- Use
pg_stat_statementsfor PostgreSQL workload analysis - Monitor CPU, memory, disk, connections, and latency together
- Correlate production metrics with deployment timestamps
- Use execution plans to understand expensive queries
- Design indexes around actual access patterns
- Watch unnecessary scans on large tables
- Avoid N+1 query behavior
- Control connection pool sizes
- Keep expensive reporting away from transactional request paths
- Cache frequently requested data where appropriate
- Monitor scheduled jobs
- Load-test important database workloads
- Establish normal performance baselines
If the same SQL is fast on a developer machine but slow under real data and concurrency, use the focused guide on why database queries become slow in production to compare plans, row estimates, locks, pool wait time, and cache effects.
Conclusion
When database CPU reaches 90%, avoid jumping directly to infrastructure changes.
Start with the workload.
Find which queries are consuming the resources.
Check whether they became slower or simply started running more frequently.
Inspect their execution plans.
Correlate the incident with traffic, connections, deployments, and background jobs.
Then apply the smallest fix that addresses the actual bottleneck.
That approach helps you distinguish between two very different situations:
An inefficient system that needs optimization
and
An efficient system that genuinely needs more capacity.
That distinction is what makes database performance debugging a production engineering skill rather than just a collection of SQL tricks.
Once an expensive statement is identified, use the EXPLAIN ANALYZE guide to compare estimates, actual rows, loops, buffers, and access paths before changing indexes or infrastructure.
References
Related
Written by
Faisal
Software engineer writing about backend systems, Node.js, system design, scalable applications, and modern web and mobile development.