Conditional View Processing in Django: ETags, Last-Modified, and the Condition Decorator

This article explains how Django handles conditional HTTP requests using ETag and Last-Modified headers. It covers the condition decorator, etag and last_modified shortcuts, how conditional processing works with unsafe HTTP methods, and how this approach compares to Django’s ConditionalGetMiddleware.

Django conditional views, ETagLast-Modified, condition decoratorIf-Modified-Since, If-None-Match, 304 Not Modified, 412 Precondition Failed

~3 min read · Updated Mar 15, 2026

Introduction

HTTP clients often send headers that indicate which version of a resource they already have. This allows servers to avoid sending full responses when nothing has changed. Django supports this through ETag and Last-Modified headers, which can be set manually or automatically via middleware.

When a client sends headers like If-Modified-Since or If-None-Match, Django can return a 304 Not Modified response instead of regenerating the full page. If the resource has changed unexpectedly, Django may return 412 Precondition Failed.


The Condition Decorator

For fine-grained control, Django provides the condition() decorator. It accepts two optional functions:

  • etag_func: returns a string ETag
  • last_modified_func: returns a datetime object

These functions receive the same arguments as the view itself and allow Django to “bail out early” if the resource has not changed.

Signature


condition(etag_func=None, last_modified_func=None)

Example

Suppose you have a blog system where the front page only changes when a new entry is published:


def latest_entry(request, blog_id):
    return Entry.objects.filter(blog=blog_id).latest("published").published

You can use this function to enable conditional responses:


from django.views.decorators.http import condition

@condition(last_modified_func=latest_entry)
def front_page(request, blog_id):
    ...

Decorator Ordering Warning

Decorators like vary_on_cookie(), vary_on_headers(), and cache_control() must appear above condition(). Conditional responses skip decorators below them, so required headers must be added first.


Shortcuts: etag() and last_modified()

If you only need one validator, Django provides simpler decorators:


etag(etag_func)
last_modified(last_modified_func)

Example


@last_modified(latest_entry)
def front_page(request, blog_id):
    ...

Or equivalently:


front_page = last_modified(latest_entry)(front_page)

Do NOT chain etag() and last_modified()

This is incorrect:


@etag(etag_func)
@last_modified(last_modified_func)
def my_view(request):
    ...

Each decorator works independently and may produce incorrect results. Use condition() when both validators are needed.


Using Conditional Processing with Unsafe Methods

Conditional validation is not limited to GET and HEAD. It also works with POST, PUT, and DELETE to prevent overwriting stale data.

Example Workflow

  1. Client GETs /foo/ and receives ETag "abcd1234".
  2. Client sends PUT with If-Match: "abcd1234".
  3. Server recomputes ETag.
  4. If ETag changed → return 412 Precondition Failed.
  5. Client fetches updated version before retrying.

This ensures safe concurrent updates.

Validator Headers for Unsafe Methods

The condition() decorator only sets ETag and Last-Modified for safe methods (GET, HEAD). If you want them on PUT or POST responses, set them manually in the view.


Comparison with ConditionalGetMiddleware

Django also provides ConditionalGetMiddleware, which handles conditional GET globally. However, it has limitations:

  • Applies to all views
  • Does not prevent expensive view processing
  • Only works for GET requests

Use condition() when:

  • You can compute ETag or last-modified quickly
  • Your view is expensive to generate
  • You need conditional logic for POST/PUT/DELETE

Use middleware when:

  • Your views are already fast
  • You only need conditional GET

Conclusion

Conditional view processing in Django provides powerful tools for optimizing performance and ensuring safe updates. With ETag and Last-Modified validators, the condition decorator, and proper use of HTTP precondition headers, you can significantly reduce unnecessary computation and bandwidth usage while maintaining data integrity.

Written & researched by Dr. Shahin Siami

Related Articles

Django Tasks Framework: A Complete Guide to Background Task Execution in Django 6.0

Django 6.0 introduces the Tasks framework, a built‑in system for defining and queuing background work outside the request–response cycle. This article explains how Tasks work, how to configure backends, how to define and enqueue tasks, how context works, and how to integrate third‑party worker systems for production environments.

Continue

Asynchronous Support in Django: A Complete Guide to Async Views, ORM, Middleware, Performance, and Safety

This article explains Django’s asynchronous (async) capabilities, including async views, ASGI support, middleware behavior, async ORM features, performance considerations, handling disconnects, and Django’s async safety protections. It also covers how to use sync_to_async(), async ORM methods, and how to safely run synchronous code in async environments.

Continue

Django System Check Framework: A Complete Guide to Writing, Registering, Running, and Testing System Checks

This article explains Django’s System Check Framework—a powerful mechanism for detecting configuration issues, validating project structure, and ensuring code quality. It covers how checks are executed, how to write custom checks, how messages work, how to register and tag checks, how to extend checks for fields and models, and how to write both unit and integration tests for system checks.

Continue

Django Signals: A Complete Guide to Listening, Connecting, Sending, and Managing Application Events

This article provides a comprehensive explanation of Django’s signal system—an event‑driven mechanism that allows decoupled applications to react to actions occurring elsewhere in the framework. It covers how to define receivers, connect signals, use decorators, handle specific senders, organize signal code, and follow best practices to avoid complexity.

Continue

Understanding Django Settings: Configuration, Environment Management, and Best Practices

This article provides a complete overview of Django’s settings system. It explains how settings files work, how to designate a settings module, how to use settings in your code, how to configure Django manually, how to secure sensitive settings, and how to work with custom default settings. It also covers the role of django.setup() for standalone scripts.

Continue

Serializing and Deserializing Django Objects: A Complete Guide to Django’s Serialization Framework

This article explains Django’s serialization framework, including how to serialize and deserialize model instances, work with subsets of fields, handle inherited models, use different serialization formats (JSON, XML, YAML, JSONL), and understand how relational fields are represented. It also covers DeserializedObject behavior and common pitfalls.

Continue