Read/write splitting with a database router
coreadvancedA replica is a copy of the primary database that receives its changes and serves reads. To use one, add it to `DATABASES` and write a router — a class with `db_for_read()`, `db_for_write()`, `allow_relation()` and `allow_migrate()` — then list it in `DATABASE_ROUTERS`. Django asks each router in turn until one returns a database name, so a router that returns `None` is saying "no opinion" rather than "use the default". Writes go to the primary, reads can go to a replica, and migrations must only ever run against the primary.
Think of it as
The router is a routing table, not a policy engine — it is consulted per query with the model and some hints, and it answers with a database alias or abstains. Keeping it that simple is what makes it predictable, because it runs on every single query and anything expensive or stateful in there is multiplied by your query rate. Two of the four methods are easy to underestimate. `allow_relation()` exists because Django refuses to relate objects that came from different databases unless a router says it is fine; with a primary and its replicas that is always fine, since they hold the same data, and forgetting it produces confusing errors when an object read from a replica is assigned to a foreign key on an object being written to the primary. `allow_migrate()` is the safety rail: a replica is maintained by replication, not by your migrations, so running `migrate` against one is either an error or — worse, if it is writable — a divergence that replication will later conflict with. Return `db == "primary"` there and you cannot make that mistake. The last piece is that routing is a default, not a rule. Django gives you `using()` on a queryset and `save(using=…)` on a model to override it per call, and the interesting reads are the ones where you deliberately do that: anything that must see a write that just happened has to name the primary explicitly, because the router has no way to know that.
What we're doing: A complete router, including the two methods people leave out, plus the settings that make a replica safe.
- 8–9
- Random choice across replicas is the spread Django's own example uses. Anything cleverer runs on every read, so keep it to arithmetic rather than a health check.
- 14–20
- The method most often omitted. Returning `None` here means "no opinion", and Django then refuses relations between objects from different aliases — which surfaces later as a confusing error in unrelated code.
- 22–24
- One line that makes an entire category of accident impossible. A `migrate` aimed at a replica either fails on a read-only server or, if the replica is writable, creates a divergence replication will conflict with.
- 29
- Leaving `default` empty forces every query to be routed or explicitly aliased. A populated `default` hides router mistakes, because anything unrouted silently works.
- 31–32
- `TEST: {"MIRROR": "primary"}` tells the test runner these are replicas of the same data rather than separate databases, so it does not create and migrate a second test database for each.
Why this works: All four methods are present, the replica aliases are declared as mirrors for tests, and an empty `default` turns a silent routing gap into an immediate error.
Writing only `db_for_read` and `db_for_write`
Wrong
Better
What you see: Two unrelated failures appear: an error about relating objects from different databases in code that never mentions databases, and a deploy that tries to run migrations against a replica.
Why: Django treats the four methods as independent questions and applies conservative defaults to the two you did not answer. For relations, the default is to refuse anything spanning aliases — correct for genuinely separate databases and wrong for a primary and its replicas, which hold the same rows. For migrations, the absence of an opinion means "go ahead", so every alias in `DATABASES` is a migration target. Both are one-line answers, and both prevent failures that surface far from the router.
- view → router: SELECT Order (list page)
- router → replica: db_for_read → "replica1" (read traffic leaves the primary alone)
- replica → view: rows
- view → router: INSERT Order
- router → primary: db_for_write → "primary" (always; a replica is read-only)
- primary → replica: replication stream (asynchronous — this is where lag lives)
- view → replica: SELECT the order just written (routed to the replica by default)
- replica → view: DoesNotExist (not an error in the router — the row has not arrived yet)
- view → primary: .using("primary") for read-your-writes (the override the router cannot infer)
The four router methods, and what happens if you skip one
Together
Remember: A router answers four independent questions, and the two people skip are the ones that fail far away: `allow_relation` must return `True` across primary and replicas or Django refuses to relate objects read from different aliases, and `allow_migrate` should permit only the primary so `migrate` can never touch a replica. Keep the router to arithmetic — it runs on every query, so a health check inside it is multiplied by your query rate. Routing is a default, not a rule: `using("primary")` is how a read that must see a recent write opts out.
See also: replica lag and reading your own writes · worker multiplication and connection exhaustion · connection limits replicas and partitioning

