Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 78

Django Celery with Redis: Offloading Tasks

Recommended resources

Hello! Welcome to the next lesson in our exploration of asynchronous processing.

In the last few lessons, we've built up a strong theoretical foundation for asynchronous task queues. We've defined the core components (producer, broker, worker), explored why idempotency is crucial, and learned how to build resilient tasks using intelligent retry strategies like exponential backoff.

Now, it's time to put all that theory into practice. This lesson is a hands-on workshop where we will achieve a core backend development task: setting up Celery with Redis to offload a long-running job from a Django view. By the end of this lesson, you will have a working system that demonstrates the power of asynchronous processing and a deeper understanding of how the components you've learned about fit together.

The Big Picture: Our Goal

Before we dive into the code, let's look at the architecture we're about to build. A user makes a request to our Django application. Instead of handling a time-consuming task within the request-response cycle and making the user wait, the Django view will immediately hand off the task to a message broker (Redis). A separate Celery worker process will then pick up the task from the broker and execute it in the background.

Celery Architecture Diagram
This diagram shows the high-level architecture we will be implementing. Our Django app is the producer, sending tasks to the Redis message broker. Celery workers consume tasks from the broker, and can optionally store results back in a result store.

This pattern is fundamental to building responsive and scalable web applications. Let's get started.


Part 1: Project Setup and Configuration

We'll be following a clear, step-by-step process. The video below provides an excellent end-to-end walkthrough of this setup. I recommend you watch it as you follow the steps outlined here. We'll also reference a blog post for easy access to code snippets.

How to set up Celery and Redis - Django Background Tasks - Part 2

This video by Andreas Jud is a complete guide to setting up Celery with Redis for a Django project. It covers installation, configuration, task creation, and execution.

You can watch this video in segments as we go through the steps. For now, you can start with the sections on installation and initial configuration (02:13 - 08:44). We will cover task creation and calling it from a view next.

For written instructions and code you can copy, this blog post from Sevalla is a great companion.

Handling long running tasks in Django apps

This article provides a step-by-step tutorial that mirrors the video's content, making it a great reference for code snippets and configuration details.

Refer to the sections 'Install Redis' and 'Install Celery' for the initial commands and configuration code.

Step 1: Install the Broker (Redis)

As we've discussed, Celery needs a message broker to hold tasks. Redis is a popular choice because it's fast, lightweight, and easy to set up.

  • If you're using a Mac with Homebrew, you can install Redis with brew install redis.
  • If you're using Docker (recommended for consistency), you can start a Redis container with:
    docker run -d -p 6379:6379 redis
    
  • On Linux, you can use your package manager, e.g., sudo apt-get install redis-server.
  • On Windows, it's best to use Redis via the Windows Subsystem for Linux (WSL), as Celery's default worker mode is not compatible with Windows.

Step 2: Install Python Dependencies

In your project's virtual environment, install Celery and the Python client for Redis:

pip install celery redis

Step 3: Configure Celery in Django

Now we need to tell Django how to communicate with Celery and Redis. This involves two main steps:

  1. Update settings.py: Add the CELERY_BROKER_URL setting. This tells Celery where our Redis broker is running.

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

    The /0 at the end specifies Redis database number 0, which is the default.

  2. Create celery.py: In your main Django project directory (the one containing settings.py), create a new file named celery.py. This file will define our Celery application instance.

    
    
    
    # myproject/celery.py
    
    import os
    from celery import Celery
    
    
    
    
    # Set the default Django settings module for the 'celery' program.
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
    
    app = Celery('myproject')
    
    
    
    
    # Using a string here means the worker doesn't have to serialize
    # the configuration object to child processes.
    # - namespace='CELERY' means all celery-related configuration keys
    #   should have a `CELERY_` prefix.
    app.config_from_object('django.conf:settings', namespace='CELERY')
    
    
    
    
    # Load task modules from all registered Django app configs.
    app.autodiscover_tasks()
    

    Make sure to replace 'myproject' with the actual name of your Django project.

  3. Import the app in __init__.py: To ensure the Celery app is loaded when Django starts, you need to import it in the __init__.py file located in the same directory as your settings.py.

    
    
    
    # myproject/__init__.py
    
    
    
    
    # This will make sure the app is always imported when
    # Django starts so that shared_task will use this app.
    from .celery import app as celery_app
    
    __all__ = ('celery_app',)
    

Your project is now configured to use Celery!


Part 2: Creating and Triggering a Task

Now for the fun part: defining a task and offloading its execution from a view.

Step 1: Define the Task

In one of your Django apps (e.g., yourapp), create a new file called tasks.py. Here, we'll define a function that simulates a long-running process and decorate it with @shared_task.




# yourapp/tasks.py

import time
from celery import shared_task

@shared_task
def simulate_long_running_task(duration_seconds):
    """
    Simulates a task that takes a while to complete.
    """
    print(f"Starting long task for {duration_seconds} seconds...")
    time.sleep(duration_seconds)
    print("Long task finished.")
    return f"Task completed after {duration_seconds} seconds."

The @shared_task decorator lets you create tasks without having to import the Celery app instance directly, which is great for reusable apps.

Step 2: Trigger the Task from a View

Now, modify a view in yourapp/views.py to call this task. Instead of running the time.sleep() directly, we'll use the .delay() method on our task function.




# yourapp/views.py

from django.http import JsonResponse
from .tasks import simulate_long_running_task

def start_task_view(request):
    """
    A view to trigger our long-running task.
    """



    # Offload the task to Celery
    task = simulate_long_running_task.delay(10) # Simulates a 10-second task
    



    # Return a response immediately
    return JsonResponse({
        "message": "Your task has been submitted!",
        "task_id": task.id # Celery returns an AsyncResult object with a task ID
    })

The .delay() method is a convenient shortcut for Celery's more general .apply_async() method. When you call it, Celery immediately serializes the function name and its arguments (10) into a message and sends it to the Redis broker. The view then returns a JsonResponse instantly, without waiting for the 10-second sleep to finish.

A Critical Detail: Serialization
Notice we're passing a simple integer (10) to our task. This is crucial. You can only pass arguments to Celery tasks that can be serialized (converted to a format like JSON). Simple data types like strings, numbers, lists, and dictionaries are fine. Passing complex objects like Django model instances or file handles will fail. A common pattern is to pass the primary key (ID) of an object and have the task re-fetch the object from the database.


Part 3: Running the System and Seeing it Work

You now have three main components that need to run concurrently in separate terminal windows:

  1. The Redis Broker: If you're using a local install, run redis-server. If you're using Docker, it should already be running.

  2. The Django Web Server:

    python manage.py runserver
    
  3. The Celery Worker:

    celery -A myproject worker -l info
    

    Replace myproject with your project name. The -A flag points to your Celery application instance.

    If you are on Windows and not using WSL, you need to use the solo worker pool:

    celery -A myproject worker -l info -P solo
    

Now, navigate to the URL connected to your start_task_view. You should get an immediate JSON response in your browser. Then, look at your Celery worker's terminal. You will see logs indicating that the worker received the task, started executing it, and finished after 10 seconds. You've successfully offloaded a long-running task!

How to set up Celery and Redis - Django Background Tasks - Part 2

Let's watch the final part of the video, where the worker is started and a common serialization error is debugged. This is an important practical step.

Watch from 10:41 to the end. Pay close attention to the celery worker command and the process of identifying and fixing the 'not JSON serializable' error, which reinforces the importance of passing simple data types to tasks.


Part 4: Under the Hood - What's Happening in Redis?

You've built a working system, which is great. But your goal is to understand things at the deepest level. So, what is actually happening inside Redis when you call .delay()?

When a task is submitted, Celery pushes a message onto a Redis List. A Celery worker then performs a blocking pop command (like BRPOP) on this list to wait for and retrieve new tasks.

Let's see this in action. You can use the Redis Command Line Interface (redis-cli) to inspect the broker.

Redis as a Message Broker: Deep Dive

This article provides a fantastic deep dive into how Celery uses Redis as a broker. We'll use it to inspect the underlying data structures.

Read the section 'Understanding Redis Internals in the Context of Celery'. This will show you exactly what keys Celery creates in Redis and how tasks are stored in a list before being processed. You can try the KEYS * and LRANGE celery 0 -1 commands in redis-cli yourself while a task is queued.

Hereโ€™s a summary of what you'll find:

  • A List as the Queue: Celery creates a Redis list (by default named celery) that acts as the task queue. New task messages are added with LPUSH.
  • Workers BRPOP: Workers use a blocking pop command to efficiently wait for and pull tasks from the right end of this list.
  • Result Metadata: If you configure a result backend, Celery will store the status and result of each task in a separate Redis key (often a String), typically named celery-task-meta-<task_id>. This is how you can check if a task succeeded and what it returned.

By inspecting Redis directly, you demystify the process and see that the "magic" of a message broker is built on top of fundamental data structures.


Conclusion

Congratulations! You have successfully configured a Django project with Celery and Redis to handle background tasks. This is a fundamental pattern for any production backend service.

Key Takeaways:

  • A task queue system consists of a producer (your Django view), a broker (Redis), and a consumer (the Celery worker).
  • Configuration involves linking these three components via settings.py and a celery.py application file.
  • Tasks are defined as simple Python functions with the @shared_task decorator.
  • Tasks are triggered asynchronously from a view using .delay(), which returns a response to the user immediately.
  • Under the hood, Celery uses a Redis List to queue task messages for workers.

Preview of the Next Lesson

In this lesson, we built the machinery for running tasks in the background. In our next and final lesson for this module, we will focus on designing a reliable background job that is idempotent and handles transient failures gracefully. We will synthesize everything we've learned about idempotency, retry strategies, and dead-letter queues to design a single, robust, production-ready task from start to finish.