Django 5.2 LTS in production: the three changes that mattered most
Django 5.2 LTS landed last spring and we treated the first six months as a holding pattern: upgrade in staging, watch the long term support ecosystem catch up, then move production. That window closed in January and we have three months of real load behind us. Three changes are worth naming; the rest behaved as the release notes promised.
Composite primary keys
Our fiscal reporting service keeps a ledger keyed on (site_id, fiscal_day, sequence). Pre 5.2 we emulated this with a unique together constraint and an auto integer surrogate, which worked but created two indexes to maintain and a constant source of confusion in ORM queries. Real composite primary keys let us drop the surrogate, shrink the table by 12%, and the part that mattered in review describe the schema in the model exactly the way the business describes it on paper.
Async ORM without the footguns
We already used async in a few places. What changed in 5.2 is that the patterns around
transactions and connection handling are finally predictable. Our ingestion worker the one that consumes
MQTT forecourt events and writes transactions moved to an async path end to end and sustained ~4.2× the
throughput on the same pod, with tail latency down rather than up. We still write sync code by
default; async is now a tool we reach for deliberately, not a risk.
Form template rendering
A small change with large consequences. Forms render through the template engine in 5.2 by default, which means our customisations stopped living in form widget subclasses and moved into templates the frontend team could read. Three weeks of accumulated widget hack tickets evaporated in a single afternoon.
What we did not use
We skipped QuerySet.none() on empty list behaviour changes for now the semantics are
correct but our existing code relied on the old shape in two reporting queries, and rewriting them was
not worth doing in the upgrade window. A task for the summer.
leave a comment