← Back to DevBytes

Huey vs Django vs FastAPI: Framework Comparison

Understanding the Landscape: Web Frameworks and Task Queues

Modern web development often requires choosing the right tools for handling HTTP requests and managing background work. This tutorial compares three distinct Python technologies that occupy different but overlapping spaces in the ecosystem: Django (a full-stack web framework), FastAPI (a high-performance async web framework), and Huey (a lightweight task queue). Understanding their strengths, use cases, and how they complement each other is essential for building scalable Python applications.

While Django and FastAPI are direct competitors in the web framework space, Huey serves a different purpose — it handles deferred, scheduled, and background task execution. However, in practice, you'll often combine a web framework with a task queue, making this three-way comparison invaluable when architecting real-world systems.

Huey: The Lightweight Task Queue

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

What Is Huey?

Huey is a minimalist task queue written in Python by Charles Leifer (the creator of Peewee ORM). It supports multi-threaded task execution, scheduled jobs, and periodic tasks (like cron), all without requiring a dedicated message broker. By default, Huey uses SQLite for storage, but it also supports Redis and other backends. This makes it perfect for small-to-medium applications, prototypes, and projects where setting up RabbitMQ or Redis is overkill.

Why Huey Matters

Core Concepts and Practical Example

Let's build a complete Huey-powered email notification system. We'll define tasks, schedule periodic cleanup, and run the consumer — all with SQLite storage.

# tasks.py - Define your background tasks
from huey import SqliteHuey
from datetime import datetime
import smtplib
import logging

# Initialize Huey with SQLite storage
# The 'tasks_db' file will be created automatically
huey = SqliteHuey('email_app', filename='tasks_db.sqlite')

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Task Definitions ---

@huey.task()
def send_welcome_email(user_email: str, username: str):
    """Simulate sending a welcome email (I/O-bound task)"""
    logger.info(f"[{datetime.now()}] Sending welcome email to {user_email}")
    # In production, connect to an SMTP server here
    # with smtplib.SMTP('smtp.example.com', 587) as server:
    #     server.starttls()
    #     server.login('user', 'password')
    #     server.sendmail(...)
    import time
    time.sleep(0.5)  # Simulate network latency
    logger.info(f"Welcome email sent to {username}!")
    return f"Email delivered to {user_email}"

@huey.task(retries=3, retry_delay=10)
def process_user_data(user_id: int, data: dict):
    """Process user data with automatic retries on failure"""
    logger.info(f"Processing data for user {user_id}")
    # Simulate potential transient failure
    if data.get('fail_simulation'):
        raise ValueError("Transient error - will retry")
    # Actual processing logic here
    total = sum(data.get('values', []))
    logger.info(f"Processed user {user_id}, result: {total}")
    return total

# --- Periodic Task (runs every 60 seconds) ---

@huey.periodic_task(60)
def cleanup_expired_sessions():
    """Periodically clean up expired user sessions"""
    logger.info(f"[{datetime.now()}] Running session cleanup")
    # In a real app, query the database and delete expired rows
    logger.info("Expired sessions removed")

Now, let's see how to enqueue tasks from your application code — whether it's a script, a Flask view, or any other Python context.

# main_app.py - Enqueue tasks from anywhere in your code
from tasks import send_welcome_email, process_user_data, cleanup_expired_sessions

# 1. Enqueue a task immediately
result = send_welcome_email('alice@example.com', 'Alice')
print(f"Task ID: {result.id}")  # Non-blocking — returns immediately

# 2. Schedule a task to run after a delay (in seconds)
delayed_result = send_welcome_email.schedule(
    ('bob@example.com', 'Bob'),
    delay=30  # Run in 30 seconds
)

# 3. Enqueue a task with retry logic
process_user_data(42, {'values': [10, 20, 30], 'fail_simulation': False})

# 4. You can also retrieve task results (if the task returns something)
# This blocks until the task completes (use cautiously)
# task_result = result.get()  # Blocks for up to timeout

To run the consumer (the worker that executes tasks), use the command line:

# Terminal: Run the Huey consumer
# This will process tasks from the SQLite database
python tasks.py huey_consumer.py -k threaded -w 4 -v

# Flags explained:
# -k threaded : Use thread pool worker (good for I/O tasks)
# -w 4        : Number of worker threads
# -v          : Verbose logging
#
# For periodic tasks, the consumer handles scheduling automatically.

Huey Best Practices

Django: The Full-Stack Framework

What Is Django?

Django is a batteries-included web framework that provides an ORM, template engine, authentication system, admin interface, and a robust request/response cycle. It follows the MVC (Model-View-Controller) pattern — or more accurately, MTV (Model-Template-View) — and emphasizes convention over configuration. Django does not include a built-in task queue; instead, it's commonly paired with Celery or, increasingly, Huey for background work.

Why Django Matters

Django with Huey: A Practical Integration

Django doesn't ship with async task support, so integrating Huey gives you lightweight background processing. Here's a complete example of a Django app that sends notification emails via Huey.

# settings.py (add to your Django settings)
# Huey configuration for Django integration
HUEY = {
    'name': 'django_app',
    'immediate': False,  # Set True for synchronous execution in tests
    'utc': True,
    'storage': {
        'type': 'sqlite',
        'path': '/tmp/django_huey.sqlite',
    },
    'consumer': {
        'workers': 4,
        'worker_type': 'thread',
    },
}
# tasks.py - Huey tasks in a Django project
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

from huey import SqliteHuey
from django.core.mail import send_mail
from django.conf import settings
from .models import Notification

huey = SqliteHuey('django_app', filename='/tmp/django_huey.sqlite')

@huey.task()
def send_notification_email(user_id: int, subject: str, body: str):
    """Send email notification to a user by ID"""
    from django.contrib.auth.models import User
    user = User.objects.get(id=user_id)
    send_mail(
        subject,
        body,
        settings.DEFAULT_FROM_EMAIL,
        [user.email],
        fail_silently=False,
    )
    # Record the notification in the database
    Notification.objects.create(
        user=user,
        subject=subject,
        body=body,
    )
    return f"Notification sent to {user.email}"

@huey.task(retries=2)
def generate_report(report_id: int):
    """Generate a PDF report (long-running task)"""
    from .models import Report
    report = Report.objects.get(id=report_id)
    # Simulate PDF generation
    import time
    time.sleep(10)
    report.status = 'COMPLETED'
    report.save()
    return f"Report {report_id} generated"
# views.py - Django view that enqueues background tasks
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .tasks import send_notification_email, generate_report
from .models import Report

@login_required
def trigger_notification(request):
    """View that fires off a background email task"""
    if request.method == 'POST':
        subject = request.POST.get('subject')
        body = request.POST.get('body')
        # Enqueue the task — non-blocking
        task = send_notification_email(
            request.user.id,
            subject,
            body
        )
        # Optionally store task_id to check status later
        request.session['last_task_id'] = task.id
        return redirect('dashboard')
    return render(request, 'notify_form.html')

@login_required
def request_report(request, report_id):
    """Kick off a long-running report generation"""
    task = generate_report(report_id)
    return render(request, 'report_pending.html', {
        'task_id': task.id,
        'report_id': report_id,
    })

To run the Huey consumer alongside Django, use a management command pattern or run it directly:

# Run the consumer (separate process from your web server)
python manage.py run_huey  # If using django-huey package
# Or directly:
python tasks.py huey_consumer.py -k threaded -w 4

Django Best Practices

FastAPI: The Modern Async Framework

What Is FastAPI?

FastAPI is a high-performance web framework built on Starlette and Pydantic. It leverages Python's async/await syntax, automatic OpenAPI documentation generation, and type hints to deliver blazing-fast APIs with minimal boilerplate. FastAPI includes BackgroundTasks for lightweight background work, but for production-grade task queues, you'll often integrate Celery or Huey.

Why FastAPI Matters

FastAPI with Huey: Async Meets Background Tasks

FastAPI's built-in BackgroundTasks is suitable for simple fire-and-forget operations, but Huey provides retries, scheduling, and persistence. Here's how to combine them effectively.

# main.py - FastAPI application with Huey integration
from fastapi import FastAPI, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from typing import Optional
import asyncio
from huey import SqliteHuey
from datetime import datetime

app = FastAPI(title="Notification API", version="1.0.0")

# Initialize Huey (shared across the application)
huey = SqliteHuey('fastapi_app', filename='huey_db.sqlite')

# --- Pydantic Models for Request/Response ---

class NotificationRequest(BaseModel):
    email: EmailStr
    subject: str
    body: str
    send_at: Optional[datetime] = None  # Optional scheduled delivery

class TaskResponse(BaseModel):
    task_id: str
    status: str
    message: str

# --- Huey Task Definitions ---

@huey.task(retries=3, retry_delay=5)
def send_email_task(email: str, subject: str, body: str):
    """Actual email sending (runs in worker thread)"""
    import smtplib
    import time
    # Simulate email sending
    time.sleep(1)
    print(f"[HUEY WORKER] Email sent to {email}: {subject}")
    return True

@huey.task()
def process_analytics(event_type: str, metadata: dict):
    """Process analytics events asynchronously"""
    time.sleep(2)
    print(f"[HUEY WORKER] Processed {event_type}: {metadata}")
    return f"Analytics for {event_type} done"

# --- FastAPI Endpoints ---

@app.post("/notify", response_model=TaskResponse)
async def send_notification(
    notification: NotificationRequest,
    background_tasks: BackgroundTasks
):
    """
    Send a notification email using Huey for reliable delivery.
    Also demonstrates FastAPI's BackgroundTasks for lightweight ops.
    """
    # Use Huey for the heavy lifting (with retries and persistence)
    if notification.send_at:
        # Schedule for future delivery
        task = send_email_task.schedule(
            (notification.email, notification.subject, notification.body),
            delay=(notification.send_at - datetime.now()).total_seconds()
        )
    else:
        task = send_email_task(notification.email, notification.subject, notification.body)
    
    # Use FastAPI's BackgroundTasks for something trivial (e.g., logging)
    async def log_event():
        await asyncio.sleep(0.1)
        print(f"[BACKGROUND] Notification requested for {notification.email}")
    
    background_tasks.add_task(log_event)
    
    return TaskResponse(
        task_id=str(task.id),
        status="queued",
        message=f"Notification queued for {notification.email}"
    )

@app.get("/task/{task_id}")
async def get_task_status(task_id: str):
    """Check the status of a Huey task"""
    # Huey stores results; you can query the task store
    # For production, consider storing task metadata in your database
    return {"task_id": task_id, "info": "Query Huey task store for status"}

@app.post("/analytics")
async def track_event(event_type: str, metadata: dict):
    """Fire-and-forget analytics processing"""
    task = process_analytics(event_type, metadata)
    return {"task_id": str(task.id), "status": "accepted"}

Run the FastAPI server and Huey consumer in separate terminals:

# Terminal 1: Start FastAPI (ASGI server)
uvicorn main:app --host 0.0.0.0 --port 8000 --reload

# Terminal 2: Start Huey consumer
python main.py huey_consumer.py -k threaded -w 4 -v

# Test with curl:
# curl -X POST http://localhost:8000/notify \
#   -H "Content-Type: application/json" \
#   -d '{"email": "user@example.com", "subject": "Hello", "body": "World"}'

FastAPI Best Practices

Side-by-Side Comparison: When to Use What

The table below summarizes the key differences and helps you make informed architectural decisions.

Criterion Huey Django FastAPI
Primary purpose Task queue / background worker Full-stack web framework High-performance API framework
Async support Thread/process-based (not async-native) Partial (ASGI support via Channels, but views are sync by default) Native async/await throughout
Built-in ORM None (pairs well with Peewee) Powerful ORM included None (use SQLAlchemy, Tortoise, or Peewee)
Admin interface No Yes — automatic admin from models No (use third-party tools like SQLAdmin)
Learning curve Low — minimal API Medium — many concepts to grasp Low-Medium — intuitive if you know async Python
Task scheduling Built-in (delay, periodic tasks) External (Celery, Huey, or django-background-tasks) External (Celery, Huey, or BackgroundTasks for simple ops)
Ideal use case Lightweight task processing for small/medium apps Content-heavy websites, admin dashboards, CMS Real-time APIs, microservices, ML model serving

Combining Them: A Real-World Architecture

In production, these tools often work together rather than compete. Here's a common pattern:

# Architecture diagram (conceptual):
#
#  FastAPI (API Gateway)
#    │
#    ├── POST /orders  →  Enqueues Huey task for order processing
#    │
#    ├── GET  /users   →  Queries database (async via SQLAlchemy)
#    │
#    └── WebSocket /ws →  Real-time notifications
#
#  Huey Worker (Background)
#    │
#    ├── process_order()        →  Validates, updates DB, sends emails
#    ├── generate_invoice_pdf() →  CPU-heavy (use process worker)
#    └── cleanup_old_records()  →  Periodic maintenance
#
#  Django Admin (Internal Tool)
#    │
#    └── /admin/  →  Staff manages users, views analytics, exports CSVs

# Concrete integration example: FastAPI + Huey + Django models
# This demonstrates using Django's ORM from a Huey task triggered by FastAPI

# In your Huey task file (used by FastAPI worker):
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
import django
django.setup()

from huey import RedisHuey
from django.contrib.auth.models import User
from django.core.mail import send_mail

huey = RedisHuey('cross_app_tasks')

@huey.task(retries=2)
def process_order_task(order_id: int):
    """Called from FastAPI, uses Django ORM for data access"""
    from orders.models import Order  # Django model
    order = Order.objects.select_related('user').get(id=order_id)
    
    # Update order status
    order.status = 'PROCESSING'
    order.save()
    
    # Send confirmation via Django's email backend
    send_mail(
        'Order Confirmed',
        f'Your order #{order.id} is being processed.',
        'noreply@example.com',
        [order.user.email],
    )
    
    order.status = 'COMPLETED'
    order.save()
    return f"Order {order_id} processed"

Decision Framework: Choosing Your Stack

Use this decision tree to pick the right tool for your context:

Common Pitfalls and How to Avoid Them

Conclusion

Huey, Django, and FastAPI each excel in their respective domains. Huey is the go-to lightweight task queue when you need background processing without infrastructure complexity — it works beautifully as a companion to any Python web framework. Django remains the king of rapid, full-featured web development with its ORM, admin interface, and battle-tested security defaults. FastAPI leads the pack for async-native, high-performance APIs with automatic documentation and modern Python type safety.

The real power emerges when you combine them strategically: FastAPI handling real-time API traffic, Huey processing background jobs reliably, and Django (or its ORM) managing complex data models. By understanding each tool's sweet spot, you can assemble a Python stack that's both pragmatic and performant — using the right tool for each layer of your application, rather than forcing one framework to do everything. Start simple with Huey for task queues, reach for Django when you need batteries included, and choose FastAPI when speed and async matter most.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles