1.Big-O where it counts: round trips, not instructions
Big-O describes how work grows as the input grows. In a web request the unit that matters is rarely a Python instruction — it is a **round trip**: one query, one cache lookup, one HTTP call. An O(n) Python loop over 500 rows in memory is not the problem. An O(n) loop that does one query per row is, because each iteration pays for a network hop and a database plan. Space complexity works the same way: what fills memory is holding *whole rows* — a `list(Order.objects.all())` grows with the table, and one large response can use more memory than the rest of the request put together.
Key features
- The unit of cost in a request is a round trip — a query, a cache `GET`, an HTTP call — not a Python statement
- O(1) queries with an O(n) Python loop is fine; O(n) queries with a tidy loop is the bug
- Constant factors inside the process rarely matter next to removing a single boundary crossing
- Space grows with rows held, not rows read: `.iterator()` streams, `list(qs)` materialises the whole table
- Per-request memory multiplies by concurrency — 40 MB per request is 1.2 GB at thirty in flight
- Django's own advice is to profile first: "remember to profile after every change to ensure that the change is a benefit"
Same loop shape, wildly different cost
- O(n) in memory — fine
- n iterations of Python work, zero extra round trips
- Cost is measured in microseconds at page-sized n
- Scales with CPU, which you have plenty of
- Optimising this is usually wasted effort
- O(n) round trips — the bug
- n iterations, each crossing a process boundary
- Cost is n × (network + plan + fetch)
- Scales with the slowest shared resource you have
- Removing the boundary is the only fix that works
Command
# count what leaves the process, not what runs inside it
with django_assert_num_queries(1):
total = sum(o.total for o in Order.objects.filter(month=9))Count what leaves the process. An O(n) Python loop over a page of rows is invisible; an O(n) loop that crosses a boundary each iteration is the bug, and no amount of comprehension-tuning touches it. Carry two numbers for every endpoint — how its round trips grow with the data, and how its memory grows with the response — because memory is the one that multiplies by concurrency and kills the whole process rather than slowing it down. Profile before changing anything: Django's own advice is to find out what your queries cost first.