Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
Performanceintermediate

CPU Bottlenecks

Telling a genuine CPU limit from queueing, and the query patterns that burn CPU unnecessarily.

3 min readIntermediateUpdated Edit this page

High CPU is not a problem by itself. A database at 70% CPU with latency inside its budget is a well-used machine. The problem is CPU saturation causing queueing.

Is it actually CPU?

Check three things before concluding the database needs more cores:

  1. Run queue length. load average above the core count means processes are waiting for CPU.
  2. The wait breakdown. If sessions are waiting on I/O or locks rather than running, the CPU is not the limit.
  3. Whether the work is necessary. Most "CPU bound" databases are executing queries that should not exist.
-- PostgreSQL: what are active sessions actually doing?
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity WHERE state = 'active'
GROUP BY 1, 2 ORDER BY 3 DESC;

A wait_event_type of NULL means genuinely running on CPU. IO means storage. Lock means contention — see Lock Contention.

Where database CPU goes

  • Query execution — the work you intend.
  • Sorting and hashing — often avoidable with the right index.
  • Parsing and planning — significant when the workload is many short, unprepared statements.
  • Compression and checksums — real, especially in column stores and on TLS connections.
  • Background maintenance — vacuum, compaction, merges. Necessary work that competes with queries.
  • Replication apply on replicas, frequently single-threaded.

The usual causes

A missing index. Scanning a million rows to return ten burns CPU on the comparison, and reading them burns I/O. This is the first thing to check.

Sorting that an index could provide. An ORDER BY that matches an index's order costs nothing; one that does not sorts the result set every time.

Row-by-row processing. A thousand single-row round trips cost far more in parsing, planning and network handling than one batched statement.

Unprepared statements. Re-parsing and re-planning identical queries with different literals. Prepared statements or a plan cache remove that cost entirely.

Over-parallelism. More parallel workers per query than the machine can run concurrently means context switching rather than throughput.

Compression settings tuned for space in a workload that is CPU-limited. This is a real trade, not a fault, and worth revisiting when CPU is the constraint.

Reducing it

  1. Fix the top queries by total time, not the slowest one. See Finding Slow Queries.
  2. Add the index that removes a scan, or the composite index that removes a sort.
  3. Batch round trips.
  4. Use prepared statements.
  5. Move analytical work off the transactional database — a replica, or an analytical store fed by change data capture.
  6. Cache what is recomputed constantly, being deliberate about the failure modes a cache introduces.

Only after those: more cores, or faster ones. Note that single-thread speed matters more than core count for engines like Redis, and for single-threaded phases like replication apply.