Skip to main content

Django Request Signals

Introduction

Django's signal framework provides a powerful way to allow decoupled applications to communicate when certain events occur. While we've explored the basic model signals in previous sections, Django also provides specialized signals related to the HTTP request-response cycle. These request signals allow your code to execute actions when requests are received or responses are about to be sent.

Request signals help you implement cross-cutting concerns like logging, monitoring, or analytics without cluttering your view logic. They're particularly useful for functionality that needs to be applied across many views consistently.

Request Signals in Django

Django provides two main request-related signals:

  1. request_started - Sent when Django begins processing an HTTP request
  2. request_finished - Sent when Django finishes processing an HTTP request

Let's explore how these signals work and how you can use them in your applications.

The Request-Response Lifecycle and Signals

Before diving into the signals, it's helpful to understand where they fit in Django's request-response cycle:

  1. A client sends an HTTP request
  2. Django's server receives the request
  3. request_started signal fires
  4. Request passes through middleware
  5. URL routing determines the view
  6. View processes the request and returns a response
  7. Response passes back through middleware
  8. request_finished signal fires
  9. Response is sent to the client

As you can see, request signals act as bookends around the entire request handling process.

Setting Up Request Signals

To use request signals, first make sure to import them from the correct module:

python
from django.core.signals import request_started, request_finished
from django.dispatch import receiver

The request_started Signal

This signal is sent when Django begins processing a request. Here's how to connect a function to it:

python
@receiver(request_started)
def my_request_started_handler(sender, **kwargs):
"""
Executed whenever a new request is received by Django

Parameters:
- sender: The handler class that triggered the signal (usually WSGIHandler)
- kwargs: Any additional parameters
"""
print("A request has started processing!")
# You can do initialization, logging, or other setup here

The request_finished Signal

This signal fires when Django finishes processing a request, right before the response is sent to the client:

python
@receiver(request_finished)
def my_request_finished_handler(sender, **kwargs):
"""
Executed when Django is about to send the response

Parameters:
- sender: The handler class that triggered the signal (usually WSGIHandler)
- kwargs: Any additional parameters
"""
print("A request has finished processing!")
# Good place for cleanup, metrics recording, etc.

Practical Examples

Example 1: Request Timing Middleware Using Signals

Let's create a solution that measures how long each request takes to process:

python
# In a file named timing.py in your app
import time
from django.core.signals import request_started, request_finished
from django.dispatch import receiver

# Store the start time in a thread-local variable
_request_start_time = {}

@receiver(request_started)
def start_timer(sender, **kwargs):
# Store the current timestamp
_request_start_time[time.thread_ident] = time.time()

@receiver(request_finished)
def log_request_time(sender, **kwargs):
thread_id = time.thread_ident
if thread_id in _request_start_time:
duration = time.time() - _request_start_time[thread_id]
print(f"Request processed in {duration:.2f} seconds")
# Clean up our thread-local storage
del _request_start_time[thread_id]

Add this to your AppConfig.ready() method to register the signals:

python
# In your app's apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'

def ready(self):
# Import the signals module to register signals
import myapp.timing

Example 2: Request Analytics Tracker

Here's a more practical example that logs analytics data for each request:

python
# analytics.py
from django.core.signals import request_started, request_finished
from django.dispatch import receiver
import logging

logger = logging.getLogger('request_analytics')

@receiver(request_started)
def track_request_start(sender, environ, **kwargs):
"""
Log the beginning of each request with its path and method

The environ parameter contains the WSGI environment
"""
if environ:
path = environ.get('PATH_INFO', 'unknown')
method = environ.get('REQUEST_METHOD', 'unknown')
logger.info(f"Request started: {method} {path}")

@receiver(request_finished)
def track_request_end(sender, **kwargs):
"""Log the completion of a request"""
logger.info(f"Request completed")

# In a real implementation, you might gather more data here
# like response status, size, etc.

Important Considerations

  1. Performance Impact: Signal handlers run on every request, so keep them lightweight to avoid performance issues.

  2. No Request Object: Unlike middleware, the request_started signal doesn't receive the full HttpRequest object, only the WSGI environment. If you need the request object, consider using middleware instead.

  3. Thread Safety: If running Django with multiple threads, make sure your signal handlers are thread-safe, as shown in the timing example.

  4. Order of Execution: You can't control the exact order in which signal handlers run if multiple handlers are connected to the same signal.

When to Use Request Signals vs. Middleware

While request signals and middleware can solve similar problems, each has its strengths:

Use Request Signals when:

  • You need extremely simple pre/post request processing
  • The functionality doesn't need the full request/response objects
  • You want to decouple the logic from your apps

Use Middleware when:

  • You need to modify the request or response objects
  • You need fine-grained control over the processing order
  • You need access to the view being processed

Beyond the built-in request signals, you can create custom signals for specific points in your request processing:

python
# In your app's signals.py
from django.dispatch import Signal

# Define custom signals
api_request_started = Signal(providing_args=["request"])
api_request_finished = Signal(providing_args=["request", "response"])

# In your views.py
from myapp.signals import api_request_started, api_request_finished

def api_view(request):
# Send our custom signal
api_request_started.send(sender=api_view, request=request)

# Process the request...
response = {...}

# Send our finish signal
api_request_finished.send(
sender=api_view,
request=request,
response=response
)

return response

Summary

Django's request signals provide a way to hook into the beginning and end of the request-response cycle. They're useful for implementing cross-cutting concerns like logging, performance monitoring, and analytics. While simpler than middleware, they provide less flexibility since they don't have full access to the request and response objects.

Key points to remember about request signals:

  • They fire at the very beginning and end of request processing
  • They're decoupled from your view logic
  • They're simpler but less powerful than middleware
  • They should be kept lightweight to avoid performance issues

Additional Resources

Exercises

  1. Implement a request signal that logs all POST requests to a specific file.
  2. Create a signal handler that counts the number of requests processed by your application and prints a report every 100 requests.
  3. Combine request signals with Django's caching framework to implement a simple request rate limiter.
  4. Build a custom signal that fires whenever a specific view is accessed, and use it to track user behavior.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)