Directing read queries to replicas reduces load on the primary database and improves performance.
Basic Router
pythonimport random class ReadWriteRouter: """ Route reads to replicas, writes to primary. """ read_replicas = ['replica1', 'replica2', 'replica3'] def db_for_read(self, model, **hints): # Random replica selection return random.choice(self.read_replicas) def db_for_write(self, model, **hints): return 'default' def allow_relation(self, obj1, obj2, **hints): return True def allow_migrate(self, db, app_label, model_name=None, **hints): return db == 'default'
Handling Replication Lag
Replicas may be slightly behind the primary:
pythonclass SmartRouter: """ Smart routing with replication lag awareness. """ def db_for_read(self, model, **hints): # If we just wrote, read from primary to avoid lag issues if hints.get('instance'): instance = hints['instance'] if hasattr(instance, '_just_saved'): return 'default' return 'replica' def db_for_write(self, model, **hints): return 'default'
Forcing Primary Reads
python# When you need fresh data user = User.objects.using('default').get(pk=user_id) # Force primary # Or with a context manager from django.db import router class ForcePrimary: """Context manager to force reads from primary. WARNING: router.routers is an undocumented internal attribute that may change across Django versions. Use with caution. """ def __enter__(self): self._original = router.routers router.routers = [] # Disable routing return self def __exit__(self, *args): router.routers = self._original # Usage with ForcePrimary(): user = User.objects.get(pk=user_id) # Always hits default
Transactions Across Databases
pythonfrom django.db import transaction # Transaction on specific database with transaction.atomic(using='analytics'): AnalyticsEvent.objects.using('analytics').create(...) # Multi-database atomic is NOT supported # Each database has its own transaction def transfer_data(): # These are SEPARATE transactions with transaction.atomic(using='default'): source.delete() # default db with transaction.atomic(using='analytics'): AnalyticsRecord.objects.create(...) # analytics db # If second fails, first is already committed!
Health-Aware Routing
pythonimport time from django.db import connections class HealthAwareRouter: """ Check replica health before routing. """ _replica_status = {} # Cache status _check_interval = 60 # Seconds def _is_healthy(self, db_alias): now = time.time() last_check = self._replica_status.get(db_alias, {}).get('checked', 0) if now - last_check > self._check_interval: try: conn = connections[db_alias] conn.ensure_connection() healthy = True except Exception: healthy = False self._replica_status[db_alias] = { 'healthy': healthy, 'checked': now } return self._replica_status.get(db_alias, {}).get('healthy', False) def db_for_read(self, model, **hints): for replica in ['replica1', 'replica2']: if self._is_healthy(replica): return replica return 'default' # Fallback to primary
Practical Configuration
python# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'myapp', 'HOST': 'primary.db.server', 'CONN_MAX_AGE': 600, }, 'replica': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'myapp', 'HOST': 'replica.db.server', 'CONN_MAX_AGE': 600, 'TEST': { 'MIRROR': 'default', # Don't create separate test db }, }, } DATABASE_ROUTERS = ['myapp.routers.ReadWriteRouter'] ```\n\n## Common Pitfalls\n\n1. **Not testing edge cases** — Always test read/write splitting with empty querysets, NULL values, and boundary conditions.\n2. **Premature optimization** — Profile queries with `.explain()` before applying complex optimizations.\n3. **Ignoring database-specific behavior** — Some read/write splitting features behave differently across PostgreSQL, MySQL, and SQLite.\n\n## Best Practices\n\n1. **Keep queries readable** — Use meaningful variable names and chain methods logically.\n2. **Test with realistic data** — Create fixtures that match production data patterns for accurate performance testing.\n3. **Document complex queries** — Add comments explaining the business logic behind non-obvious query patterns.\n\n## Summary\n\n- Read/Write Splitting is a core Django ORM feature for building efficient database queries.\n- Always consider query performance and use `.explain()` to verify query plans.\n- Test edge cases including empty results, NULL values, and large datasets.\n- Refer to the Django documentation for database-specific behavior and limitations.
Code Examples
python
class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
return 'replica'
def db_for_write(self, model, **hints):
return 'default'
def allow_relation(self, obj1, obj2, **hints):
return True
def allow_migrate(self, db, app_label, **hints):
return db == 'default'