๐Ÿ”ฅ Optimizations 101

How to think about performance under load โ€” before you reach for threads, processes, or async. Measure the bottleneck, then optimize it.

The Golden Rule: Measure First

Every optimization in this collection is worthless if you apply it to the wrong bottleneck. The #1 mistake under load is guessing. Adding 200 threads to a CPU-bound service makes it slower. Rewriting to async when your DB is the limit changes nothing.

Always answer this first: is the system CPU-bound (maxing cores), I/O-bound (waiting on network/disk/DB), or memory-bound (paging, GC thrash, OOM)? The answer dictates the entire strategy โ€” and it's cheap to find out with a profiler.

Know Your Bottleneck โ†’ Know Your Tool

BottleneckSymptom under loadPrimary weapon
I/O-boundLow CPU, high wait, requests pile upAsync or threads + pooling, batching, concurrency caps
CPU-boundAll cores at 100%, GIL contentionmultiprocessing / C extensions / chunking
Memory-boundRSS climbs, GC pauses, OOM killsStreaming, generators, bounded queues, __slots__
Downstream-boundA slow dependency (DB, API) saturatesRate limiting, circuit breakers, caching, backpressure
The three models, in one line each: Threads โ€” overlap blocking I/O with existing sync libraries. Async โ€” overlap tens of thousands of I/O tasks cheaply on one core. Multiprocessing โ€” the only one that gives real CPU parallelism in Python. Full comparison here.

Throughput vs Latency (They Fight)

Under load these two pull in opposite directions, and optimizing blindly for one wrecks the other.

flowchart LR L["Rising load"] --> Q["Queues fill up"] Q --> T["Throughput plateaus
(at max capacity)"] Q --> P["p99 latency spikes
(waiting in queue)"] P --> D["Timeouts & retries
โ†’ MORE load"] D --> COL["๐Ÿ’ฅ congestion collapse"]
Little's Law: concurrency = throughput ร— latency. If each request takes 100ms and you want 1000 req/s, you need ~100 in flight at once. This one formula sizes your pools, your semaphores, and your queues.

The Ceiling: Amdahl's Law

Parallelism has a hard limit. If 10% of your work is inherently serial, then even with infinite cores you can never go more than 10ร— faster. Chasing more workers past that point just adds coordination overhead.

# Speedup ceiling with a serial fraction s and N workers:
#   speedup(N) = 1 / (s + (1 - s) / N)
# s = 0.10 (10% serial), N -> infinity  =>  max speedup = 1 / 0.10 = 10x
# The lesson: kill the serial bottleneck (locks, single DB, GIL) FIRST.

The Load-Optimization Playbook

Every page in this collection is one move in this loop. Run it in order:

flowchart TB M["1. Measure
(profile, load-test)"] --> B["2. Find the bottleneck
(CPU / IO / mem / downstream)"] B --> F["3. Fix the biggest one
(pool, batch, cap, cache, parallelize)"] F --> P["4. Protect it
(timeouts, backpressure, limits)"] P --> M
  1. Reuse, don't recreate โ€” connections, clients, processes (pooling).
  2. Do less work โ€” cache and batch.
  3. Bound everything โ€” concurrency caps, bounded queues, rate limits.
  4. Parallelize the CPU part โ€” multiprocessing.
  5. Survive failure โ€” timeouts, retries, circuit breakers.
  6. Watch memory โ€” stream, don't buffer.
  7. Verify โ€” profile and benchmark the change.
The mantra: Reuse โ†’ Do less โ†’ Bound it โ†’ Parallelize โ†’ Protect it โ†’ Measure again. Optimization without measurement is just superstition.