Build a Notification Service from Scratch — Part 4: Scaling and Deployment

Final part — scale the service for 100K+ notifications/minute and deploy.

Step 1: Horizontal Scaling with Celery

Run multiple Celery workers with concurrency:

# 4 workers, 10 threads each = 40 concurrent deliveries
celery -A app.celery_app worker --concurrency=10 -n worker1@%h
celery -A app.celery_app worker --concurrency=10 -n worker2@%h
celery -A app.celery_app worker --concurrency=10 -n worker3@%h
celery -A app.celery_app worker --concurrency=10 -n worker4@%h

Step 2: Batch Processing

For bulk notifications (e.g., marketing emails), batch processing reduces overhead:

@celery_app.task(bind=True)
def send_batch(self, notification_ids: list[int]):
    db = SessionLocal()
    notifications = db.query(Notification).filter(Notification.id.in_(notification_ids)).all()

    # Group by channel for efficient dispatch
    by_channel = {"email": [], "sms": [], "push": []}
    for n in notifications:
        by_channel[n.channel].append(n)

    # Batch email via SendGrid API
    if by_channel["email"]:
        emails = [{"to": n.recipient, "subject": n.subject, "body": n.body} for n in by_channel["email"]]
        sendgrid_client.mail.send(request_body={"personalizations": emails})

    db.commit()
    db.close()

Step 3: docker-compose for Production

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://postgres:pass@db:5432/notifications
      REDIS_URL: redis://redis:6379
    depends_on:
      - db
      - redis

  worker:
    build: .
    command: celery -A app.celery_app worker --concurrency=10
    environment:
      DATABASE_URL: postgresql://postgres:pass@db:5432/notifications
      REDIS_URL: redis://redis:6379
    depends_on:
      - db
      - redis
    deploy:
      replicas: 3

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: notifications

  redis:
    image: redis:7-alpine

volumes:
  pgdata:

Step 4: Performance Tuning

# Connection pooling
engine = create_engine(
    settings.database_url,
    pool_size=50,
    max_overflow=100,
    pool_recycle=3600
)

# Redis optimizations
redis_client = redis.Redis(
    max_connections=500,
    socket_keepalive=True
)

Series Summary

We built a production notification service with:

  • Multi-channel support (email, SMS, push)
  • Jinja2 templating
  • Celery + Redis job queue with retries
  • Rate limiting per channel
  • Dead letter queue
  • Prometheus metrics and alerting
  • Horizontal scaling and Docker deployment

Expect 100K+ notifications/minute with 4 workers under $50/month infrastructure cost.

← Part 3


Advertisement