Premier League Predictor: Django & MySQL — Chapter 11, Exercise 2 ==================================================== TASK Explain why Django's own default CONN_MAX_AGE=0 creates a genuinely different kind of problem than SQLAlchemy's own pooling defaults do for the PostgreSQL sibling, then calculate the real number of persistent MySQL connections a gunicorn -w 4 --threads 2 deployment can hold open at once with CONN_MAX_AGE=60 in place. SOLUTION SQLAlchemy's create_engine() against PostgreSQL pools connections by default — each worker process keeps a small pool of already-open connections ready and reuses them across requests, which is efficient per request but means the real cost shows up in how many connections end up open at once across every worker process combined (the sibling's own worked example: 4 workers x 15 connections each = up to 60 real connections, a connection-COUNT problem). Django's own MySQL backend, with the unmodified default CONN_MAX_AGE=0, does the opposite: it opens a genuinely new connection at the start of every single request and closes it again at the end of that same request, regardless of which worker handled it. There's no pool being built up here at all, so there's no connection-count explosion risk the way SQLAlchemy's default creates. Instead, the real cost is paid per request, as a fresh TCP handshake plus a real MySQL authentication exchange on every single page view — a genuine LATENCY problem, not a connection-count problem, because at any given instant a worker process is only ever using at most the one connection it needs for whatever request it's actively handling right now. Setting CONN_MAX_AGE=60 changes this by letting a connection survive past the end of one request and be reused by the next request handled by that same thread, for up to 60 idle seconds. This is where a real connection-count question reappears, but calculated differently than for the sibling: Django holds one connection per THREAD, not per worker process. With -w 4 --threads 2, there are 4 worker processes, each running 2 threads, giving 4 x 2 = 8 distinct threads total across the whole deployment. Each of those 8 threads can independently hold its own persistent connection open under CONN_MAX_AGE=60, so the real peak connection count is 4 x 2 = 8 — not 4 (which would undercount by ignoring threads entirely) and not some larger pooled figure the way the PostgreSQL sibling's own math produces. 8 real connections sits comfortably under MySQL's own default max_connections of 151, with substantial headroom left for anything else (an admin shell session, a monitoring tool) connecting to the same database at the same time. WHY THIS WORKS AS AN ANSWER ---------------------------- It contrasts the two real failure modes precisely (a per-request latency cost from Django's own zero-persistence default vs. a connection-count explosion from SQLAlchemy's own pooling default), and correctly identifies that Django's connections are held per thread — not per process — to arrive at the real 4 x 2 = 8 figure rather than stopping at the worker count alone.