Email|Call
Back to Blog
Engineering

Configure a Read Replica Database in Django

The following article will discuss issues related to database performance and approaches that can be adopted to mitigate them using read-replicas.

DTM Team•Engineering
February 28, 2023
12 min read
Configure a Read Replica Database in Django

The Database Bottleneck

The following article will discuss issues related to database performance and approaches that can be adopted to mitigate them using read-replicas.

As applications scale, the database often becomes the bottleneck. Read-heavy applications can benefit significantly from read replicas.

What Are Read Replicas?

Read replicas are copies of your primary database that stay synchronized through replication:

┌─────────────┐       ┌─────────────┐
│   Primary   │──────▶│   Replica   │
│  (writes)   │  sync │  (reads)    │
└─────────────┘       └─────────────┘
       │                     │
       ▼                     ▼
   INSERT, UPDATE        SELECT
   DELETE                (read-only)

Benefits

  • Reduced load on primary: Distribute read queries
  • Improved read performance: Scale reads horizontally
  • Geographic distribution: Place replicas closer to users
  • Failover capability: Promote replica if primary fails

Django Database Configuration

Configure multiple databases in settings.py:

python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myapp',
        'USER': 'myapp',
        'PASSWORD': 'password',
        'HOST': 'primary.db.example.com',
        'PORT': '5432',
    },
    'replica': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myapp',
        'USER': 'myapp_readonly',
        'PASSWORD': 'password',
        'HOST': 'replica.db.example.com',
        'PORT': '5432',
    }
}

Database Router

Create a router to direct queries:

python
# myapp/db_routers.py

class PrimaryReplicaRouter:
    """
    Route reads to replica, writes to primary.
    """
    
    def db_for_read(self, model, **hints):
        """
        Reads go to replica.
        """
        return 'replica'
    
    def db_for_write(self, model, **hints):
        """
        Writes go to primary.
        """
        return 'default'
    
    def allow_relation(self, obj1, obj2, **hints):
        """
        Relations between objects are allowed.
        """
        return True
    
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Migrations only on primary.
        """
        return db == 'default'

Configure the router in settings:

python
DATABASE_ROUTERS = ['myapp.db_routers.PrimaryReplicaRouter']

Handling Replication Lag

Replicas may be slightly behind the primary. Handle this:

python
class ReplicationAwareRouter:
    """
    Router that considers replication lag.
    """
    
    def db_for_read(self, model, **hints):
        # Check if we need fresh data
        if hints.get('fresh', False):
            return 'default'
        return 'replica'

Usage in views:

python
# Normally reads from replica
users = User.objects.all()

# Force read from primary for fresh data
user = User.objects.using('default').get(pk=user_id)

Context Manager for Transactions

When you need writes and immediate reads:

python
from django.db import transaction

class UsePrimaryDatabase:
    """
    Context manager to force primary database.
    """
    
    def __enter__(self):
        # Implementation to route to primary
        pass
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # Restore normal routing
        pass

# Usage
with UsePrimaryDatabase():
    user = User.objects.create(name="John")
    # Immediately read the user we just created
    user.refresh_from_db()

Multiple Replicas

For high read loads, use multiple replicas:

python
import random

class MultiReplicaRouter:
    replicas = ['replica1', 'replica2', 'replica3']
    
    def db_for_read(self, model, **hints):
        return random.choice(self.replicas)
    
    def db_for_write(self, model, **hints):
        return 'default'

Health Checking

Monitor replica health:

python
from django.db import connections

def check_replica_health(replica_name):
    try:
        cursor = connections[replica_name].cursor()
        cursor.execute("SELECT 1")
        return True
    except Exception:
        return False

class HealthAwareRouter:
    def db_for_read(self, model, **hints):
        for replica in ['replica1', 'replica2']:
            if check_replica_health(replica):
                return replica
        return 'default'  # Fallback to primary

Best Practices

  1. Monitor replication lag: Alert if lag exceeds threshold
  2. Test failover: Practice promoting replicas
  3. Use connection pooling: PgBouncer or similar
  4. Index replicas: Ensure proper indexes for read queries
  5. Plan for writes: Primary must handle all writes

Conclusion

Read replicas are a proven pattern for scaling database performance. Django's database routing makes implementation straightforward.

Need help scaling your Django application? Contact Datamart to discuss your architecture.

Have a project in mind?

Let's discuss how Datamart can help bring your ideas to life with our expertise in software development.

Get in Touch