Skip to content

Performance Bottlenecks

A bottleneck is the constraint that limits overall throughput. Fixing anything else first is wasted effort. Find the bottleneck, fix it, repeat.


Backend (Application Server)

CPU Saturation

Symptoms: latency increases linearly with load, CPU at 90–100%.

Causes: - Compute-heavy logic running synchronously in request handlers - Unoptimized serialization / deserialization - Excessive logging with synchronous I/O

Fix: profile with py-spy or cProfile, move heavy work to background tasks.


Thread / Worker Exhaustion

Symptoms: latency spikes at a specific RPS, queue depth grows, errors appear suddenly.

Causes: - All WSGI/ASGI workers are blocked waiting for DB or downstream services - Thread pool too small for the concurrency level

Fix: increase worker count, switch to async (FastAPI + asyncio), or add connection pooling.


Blocking Operations

Symptoms: high latency despite low CPU.

Causes: - Synchronous file I/O in async context - time.sleep() in request handler - Synchronous HTTP calls to external services

Fix: use async I/O (aiofiles, httpx async client), offload blocking calls.


Database

Slow Queries

Symptoms: DB CPU low, but query time high. EXPLAIN shows sequential scans.

Fix: add indexes for WHERE/ORDER BY columns. Use EXPLAIN ANALYZE to verify.


Missing Indexes

A full table scan on 10M rows takes seconds. An indexed lookup takes microseconds. Missing indexes are the single most common cause of DB bottlenecks.

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;
-- Seq Scan → no index. Add:
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);

Lock Contention

Symptoms: queries are fast in isolation, slow under concurrent load.

Causes: - Long-running transactions holding row locks - SELECT FOR UPDATE on hot rows - Schema migrations running in production

Fix: shorten transactions, use optimistic locking, schedule migrations off-peak.


N+1 Queries

Symptoms: 100 items in a list → 101 DB queries. Latency scales with result set size.

Fix: use JOIN or ORM eager loading (select_related, prefetch_related in Django).


Network

Bandwidth Limits

Symptoms: throughput plateaus, no CPU/DB pressure.

Fix: compress responses (gzip/brotli), use pagination, reduce payload size.


High Network Latency

Symptoms: fast server processing, slow total response time.

Causes: cross-region service calls, no connection reuse.

Fix: use persistent connections, HTTP/2, place services in the same availability zone.


Application Logic

Anti-pattern Impact Fix
N+1 queries O(n) DB calls per request Eager load / JOIN
Synchronous external calls Latency = sum of all calls Parallelize with asyncio
Unbounded result sets Memory + time grows with data Paginate
In-memory sorting of large sets CPU spike Sort in DB with indexes
Regex on every request CPU-heavy for long strings Cache compiled patterns