Backend Switching Guide

Learn how to migrate between different backends (Gunicorn to Uvicorn, Celery to Django Tasks, etc.) with step-by-step instructions and configuration examples.

Overview

django-prodserver makes it easy to switch backends by simply changing configuration. This guide covers common migration scenarios and provides migration checklists.

Why Switch Backends?

Common reasons to switch:

  • Performance: Need better throughput or lower latency

  • Features: Require async support, WebSockets, or specific capabilities

  • Simplicity: Move to simpler solution for easier maintenance

  • Platform: Deploy to different operating system (Linux to Windows)

  • Cost: Reduce infrastructure dependencies

Migration Strategies

Strategy 1: Blue-Green Deployment

  1. Deploy new backend alongside old one

  2. Test thoroughly

  3. Switch traffic to new backend

  4. Keep old backend as backup

  5. Remove old backend after stabilization

Strategy 2: Gradual Migration

  1. Update configuration

  2. Deploy to staging first

  3. Test extensively

  4. Deploy to production

  5. Monitor and rollback if needed

Web Server Migrations

Gunicorn → Uvicorn (Adding Async Support)

When: You want to use async views, WebSockets, or ASGI features

Before (Gunicorn - WSGI):

PRODUCTION_PROCESSES = {
    "web": {
        "BACKEND": "django_prodserver.backends.servers.gunicorn.GunicornServer",
        "ARGS": {
            "bind": "0.0.0.0:8000",
            "workers": "4",
            "timeout": "60",
        }
    }
}

After (Uvicorn - ASGI):

PRODUCTION_PROCESSES = {
    "web": {
        "BACKEND": "django_prodserver.backends.servers.uvicorn.UvicornServer",
        "ARGS": {
            "host": "0.0.0.0",
            "port": "8000",
            "workers": "4",
            "timeout-keep-alive": "60",
        }
    }
}

Migration Steps:

  1. Install Uvicorn:

    pip install uvicorn[standard]
    
  2. Verify ASGI configuration:

    # Ensure asgi.py exists and is correct
    # myproject/asgi.py
    import os
    from django.core.asgi import get_asgi_application
    
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
    application = get_asgi_application()
    
  3. Update configuration (as shown above)

  4. Test in staging:

    python manage.py server web --settings=myproject.settings.staging
    
  5. Deploy to production

Considerations:

  • ARGS mapping differs (bind → host/port)

  • Fewer workers needed with async (1-2 per CPU)

  • Monitor performance and adjust

Gunicorn → Granian (Performance Upgrade)

When: You want Rust-powered performance while keeping WSGI

Before (Gunicorn):

PRODUCTION_PROCESSES = {
    "web": {
        "BACKEND": "django_prodserver.backends.servers.gunicorn.GunicornServer",
        "ARGS": {
            "bind": "0.0.0.0:8000",
            "workers": "4",
        }
    }
}

After (Granian WSGI):

PRODUCTION_PROCESSES = {
    "web": {
        "BACKEND": "django_prodserver.backends.servers.granian.GranianWSGIServer",
        "ARGS": {
            "address": "0.0.0.0",
            "port": "8000",
            "workers": "4",
            "threads": "2",
        }
    }
}

Migration Steps:

  1. Install Granian:

    pip install granian
    
  2. Update configuration

  3. Tune threads (Granian-specific)

  4. Test and deploy

Waitress → Gunicorn (Windows → Linux)

When: Migrating from Windows to Linux servers

Before (Waitress - Windows):

PRODUCTION_PROCESSES = {
    "web": {
        "BACKEND": "django_prodserver.backends.servers.waitress.WaitressServer",
        "ARGS": {
            "host": "0.0.0.0",
            "port": "8000",
            "threads": "6",
        }
    }
}

After (Gunicorn - Linux):

PRODUCTION_PROCESSES = {
    "web": {
        "BACKEND": "django_prodserver.backends.servers.gunicorn.GunicornServer",
        "ARGS": {
            "bind": "0.0.0.0:8000",
            "workers": "4",
        }
    }
}

Migration Steps:

  1. Prepare Linux environment

  2. Install Gunicorn:

    pip install gunicorn
    
  3. Update configuration

  4. Test on Linux staging server

  5. Migrate production

Worker Migrations

Celery → Django Tasks (Simplification)

When: Reducing complexity, removing Redis/RabbitMQ dependency

Before (Celery):

# settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'

PRODUCTION_PROCESSES = {
    "worker": {
        "BACKEND": "django_prodserver.backends.workers.celery.CeleryWorker",
        "APP": "myproject.celery.app",
        "ARGS": {
            "concurrency": "4",
        }
    },
    "beat": {
        "BACKEND": "django_prodserver.backends.workers.celery.CeleryBeat",
        "APP": "myproject.celery.app",
        "ARGS": {}
    }
}

After (Django Tasks - Django 5.1+):

# settings.py
INSTALLED_APPS = [
    # ...
    'django.contrib.tasks',
]

PRODUCTION_PROCESSES = {
    "worker": {
        "BACKEND": "django_prodserver.backends.workers.django_tasks.DjangoTasksWorker",
        "ARGS": {
            "processes": "4",
        }
    }
}

Migration Steps:

  1. Ensure Django 5.1+:

    pip install "django>=5.1"
    
  2. Add to INSTALLED_APPS

  3. Run migrations:

    python manage.py migrate
    
  4. Convert tasks:

    # Before (Celery)
    from celery import shared_task
    
    @shared_task
    def send_email(email):
        # ...
    
    # After (Django Tasks)
    from django.contrib.tasks import task
    
    @task()
    def send_email(email):
        # ...
    
  5. Update task calls (unchanged):

    send_email.delay('user@example.com')
    
  6. Test thoroughly (some Celery features not available)

  7. Remove Celery and broker

Limitations:

  • No chains, groups, or chords

  • Database-backed (less performant at scale)

  • Simpler feature set

Django Tasks → Celery (Scaling Up)

When: Need distributed processing, complex workflows, or high volume

Before (Django Tasks):

PRODUCTION_PROCESSES = {
    "worker": {
        "BACKEND": "django_prodserver.backends.workers.django_tasks.DjangoTasksWorker",
        "ARGS": {
            "processes": "2",
        }
    }
}

After (Celery):

# Requires broker setup
CELERY_BROKER_URL = 'redis://localhost:6379/0'

PRODUCTION_PROCESSES = {
    "worker": {
        "BACKEND": "django_prodserver.backends.workers.celery.CeleryWorker",
        "APP": "myproject.celery.app",
        "ARGS": {
            "concurrency": "4",
        }
    },
    "beat": {
        "BACKEND": "django_prodserver.backends.workers.celery.CeleryBeat",
        "APP": "myproject.celery.app",
        "ARGS": {}
    }
}

Migration Steps:

  1. Set up broker (Redis or RabbitMQ)

  2. Install Celery:

    pip install celery[redis]
    
  3. Create Celery app (see Worker)

  4. Convert tasks (decorator change)

  5. Update configuration

  6. Test with broker

  7. Deploy

Celery → Django-Q2 (ORM-backed Alternative)

When: Want more features than Django Tasks but simpler than Celery

Before (Celery):

PRODUCTION_PROCESSES = {
    "worker": {
        "BACKEND": "django_prodserver.backends.workers.celery.CeleryWorker",
        "APP": "myproject.celery.app",
        "ARGS": {"concurrency": "4"}
    }
}

After (Django-Q2):

# settings.py
INSTALLED_APPS = [
    # ...
    'django_q',
]

Q_CLUSTER = {
    'name': 'myproject',
    'workers': 4,
    'timeout': 90,
}

PRODUCTION_PROCESSES = {
    "worker": {
        "BACKEND": "django_prodserver.backends.workers.django_q2.DjangoQ2Worker",
        "ARGS": {"verbosity": "1"}
    }
}

Migration Steps:

  1. Install Django-Q2:

    pip install django-q2
    
  2. Add to INSTALLED_APPS

  3. Run migrations

  4. Convert tasks:

    # Before (Celery)
    from celery import shared_task
    
    @shared_task
    def process_data(data_id):
        # ...
    
    # After (Django-Q2)
    def process_data(data_id):
        # Regular function, no decorator
    
    # Enqueue differently
    from django_q.tasks import async_task
    async_task('myapp.tasks.process_data', data_id)
    
  5. Test

  6. Remove Celery and broker

Migration Checklist

Pre-Migration

  • [ ] Read new backend documentation

  • [ ] Understand ARGS differences

  • [ ] Test in local development

  • [ ] Test in staging environment

  • [ ] Prepare rollback plan

  • [ ] Document configuration changes

  • [ ] Brief team on changes

During Migration

  • [ ] Update dependencies (requirements.txt)

  • [ ] Update configuration (settings.py)

  • [ ] Convert tasks if needed

  • [ ] Update environment variables

  • [ ] Update Docker/systemd configs

  • [ ] Deploy to staging

  • [ ] Run integration tests

  • [ ] Monitor performance

  • [ ] Fix any issues

Post-Migration

  • [ ] Deploy to production

  • [ ] Monitor logs and errors

  • [ ] Check performance metrics

  • [ ] Verify all features work

  • [ ] Update documentation

  • [ ] Clean up old dependencies

  • [ ] Remove old configuration

Testing Your Migration

Functionality Tests

# Test web server
curl http://localhost:8000/
curl http://localhost:8000/admin/

# Test task processing
python manage.py shell
>>> from myapp.tasks import my_task
>>> my_task.delay()

Performance Tests

# Load testing with apache bench
ab -n 1000 -c 10 http://localhost:8000/

# Monitor resource usage
top
htop
docker stats

Rollback Plan

Keep old configuration for quick rollback:

# settings.py
USE_NEW_BACKEND = os.getenv('USE_NEW_BACKEND', 'False') == 'True'

if USE_NEW_BACKEND:
    PRODUCTION_PROCESSES = {
        # New backend config
    }
else:
    PRODUCTION_PROCESSES = {
        # Old backend config
    }

Common Issues

ARGS Not Working

Problem: Configuration not being applied

Solution: Check ARGS mapping for new backend

  • Different backends use different argument names

  • Consult backend-specific documentation

Performance Regression

Problem: New backend is slower

Solution: Tune configuration

  • Adjust worker/thread counts

  • Check resource limits

  • Monitor bottlenecks

Features Missing

Problem: Feature worked in old backend but not new one

Solution: Check feature compatibility

  • Read limitations in backend documentation

  • Consider alternative approaches

  • May need to keep old backend for specific features