Celery + Redis: Asynchronous Python email task worker pipeline with AutoSend

How do you implement non-blocking background email dispatch tasks in Python using Celery and AutoSend with retry policies for temporary network failures?

Celery + Redis Task Pipeline for AutoSend

When dispatching transactional emails in Python (Django, FastAPI, Flask), performing synchronous HTTP requests inside request-response lifecycles adds unwanted latency and risks request timeouts if the external email API or network experiences transient delays. Offloading email dispatch to Celery background workers with Redis ensures non-blocking request handlers and reliable delivery with exponential backoff retries.


1. Define Celery Worker & AutoSend Email Task

Configure a Celery application and define an email dispatch task using requests with built-in retry policies and 429 rate limit handling:

# tasks.py
import os
import requests
from celery import Celery

# Initialize Celery app with Redis broker
app = Celery('mailer_app', broker=os.getenv('REDIS_URL', 'redis://localhost:6379/0'))

AUTOSEND_API_KEY = os.getenv('AUTOSEND_API_KEY')
AUTOSEND_ENDPOINT = 'https://api.autosend.com/v1/mails/send'

@app.task(bind=True, max_retries=3, default_retry_delay=5)
def send_transactional_email_task(self, recipient: str, subject: str, html_body: str):
    """
    Background worker task to dispatch transactional emails via AutoSend REST API.
    Retries automatically with exponential backoff on transient network or 429 rate limits.
    """
    headers = {
        'Authorization': f'Bearer {AUTOSEND_API_KEY}',
        'Content-Type': 'application/json',
    }
    payload = {
        'from': {'email': 'noreply@yourdomain.com', 'name': 'App Notifications'},
        'to': [{'email': recipient}],
        'subject': subject,
        'html': html_body,
    }

    try:
        response = requests.post(AUTOSEND_ENDPOINT, json=payload, headers=headers, timeout=10)
        
        # Handle rate limiting with specific backoff countdown
        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 10))
            raise self.retry(countdown=retry_after)
            
        response.raise_for_status()
        return response.json()
        
    except requests.exceptions.RequestException as exc:
        # Exponential backoff: 5s, 10s, 20s
        countdown = 5 * (2 ** self.request.retries)
        raise self.retry(exc=exc, countdown=countdown)

2. Dispatching from FastAPI / Flask / Django

Trigger the background task asynchronously using .delay():

# app.py (FastAPI example)
from fastapi import FastAPI
from tasks import send_transactional_email_task

app = FastAPI()

@app.post("/auth/register")
def register_user(email: str, name: str):
    # 1. Create user account logic...
    
    # 2. Dispatch welcome email asynchronously to Celery worker (non-blocking)
    send_transactional_email_task.delay(
        recipient=email,
        subject="Welcome to Our Platform!",
        html_body=f"<h1>Welcome, {name}!</h1><p>Your account is ready.</p>"
    )
    
    return {"status": "success", "message": "User registered and email queued."}

3. Running the Celery Worker

Start the worker process in production or locally:

celery -A tasks worker --loglevel=info --concurrency=4

Key Engineering Practices:

  • Rate-Limit & 429 Awareness: Reading the Retry-After header during high-volume spikes allows Celery tasks to wait rather than failing outright.
  • Payload Structure: AutoSend’s /v1/mails/send endpoint expects 'to': [{'email': '...'}] or recipient objects and from as a structured object with verified email.
  • Connection Isolation: Always store AUTOSEND_API_KEY in environment variables rather than hardcoding in task modules.

Relevant Documentation