Incremental Static Revalidation for Django (SWR + tag-based invalidation + warmup + CDN connectors).
Project description
django-edge-isr
Incremental Static Revalidation for Django — the speed of static, the freshness of dynamic. Serve fast cached pages from a CDN (or any proxy), while revalidating in the background and regenerating only what changed.
⚠️ Status: Alpha design. This README outlines the vision and MVP scope. Contributions & feedback welcome.
Documentation
- Site (latest) : https://hamabarhamou.github.io/django-edge-isr/
- Quickstart : https://hamabarhamou.github.io/django-edge-isr/quickstart/
- Concepts : https://hamabarhamou.github.io/django-edge-isr/concepts/
- API Reference : https://hamabarhamou.github.io/django-edge-isr/api/
- Admin / Status Endpoints : https://hamabarhamou.github.io/django-edge-isr/admin/
- Revalidation Pipeline : https://hamabarhamou.github.io/django-edge-isr/revalidation/
- Deployment : https://hamabarhamou.github.io/django-edge-isr/deployment/
- Troubleshooting : https://hamabarhamou.github.io/django-edge-isr/troubleshooting/
- Contributing Guide : https://hamabarhamou.github.io/django-edge-isr/contributing/
- MVP architecture & release plan : https://github.com/HamaBarhamou/django-edge-isr/blob/develop/ARCHITECTURE.md
Why this project exists (and why now)
Caching in Django is powerful, but keeping pages fresh without over-purging is still hard:
- TTL vs correctness: either wait for TTLs (stale content) or “purge everything” (origin stampede).
- Ad-hoc invalidation: per-view/model logic is brittle and duplicated across projects.
- CDN gap: Django’s cache framework doesn’t natively speak modern CDN semantics like stale-while-revalidate (SWR), on-demand revalidation, or precise purge-by-URL.
- Fragment/page mapping is missing: most stacks lack a first-class way to say “this page depends on these objects”.
At the same time, modern CDNs and reverse proxies handle SWR and fast purges well. Front-end ecosystems popularized ISR (incremental static revalidation). django-edge-isr brings that developer experience to Django, in a framework-native way.
What gap does it fill?
- ✅ Tag-based invalidation: declare dependencies (
post:42,category:7) for pages/fragments. - ✅ SWR by default: serve instantly; revalidate in the background.
- ✅ On-demand, targeted revalidation driven by model signals.
- ✅ CDN connectors (opt-in): purge only affected URLs (Cloudflare/CloudFront).
- ✅ Warmup pipeline: repopulate cache asynchronously to avoid cold starts.
- ✅ Zero vendor lock-in: also works with plain reverse proxies or just Django as origin.
This is not a static site generator; it slots into any Django app and makes your dynamic pages cacheable and correct at the edge.
How it’s different from existing approaches
- Django cache middlewares: great for simple TTL caching, but no tag graph, no SWR revalidation, no URL-level CDN purges.
- Query/object caches: helpful to speed up ORM, but they don’t solve page invalidation at the edge nor background warmup.
- CMS-specific caches: work well in their ecosystem, but aren’t general-purpose and rarely expose a tag graph for arbitrary apps.
django-edge-isr focuses on page/fragment correctness at the edge with a Redis-backed tag graph, SWR headers, and a revalidation pipeline that talks to your CDN only when needed.
What you get
@isr(...)decorator for views (and template fragments) with tags, TTLs and SWR.- Signal helpers (e.g. on
post_save/post_delete) to trigger revalidation by tags. - Tag graph in Redis mapping
url ↔ tags. - Connectors for Cloudflare / CloudFront (opt-in).
- Admin endpoints to inspect URLs/tags and warmups.
- Queue adapters (Celery/RQ or in-process) for warmups & revalidation tasks.
Quickstart (MVP sketch)
# settings.py
INSTALLED_APPS += ["edge_isr"]
EDGE_ISR = {
"REDIS_URL": "redis://localhost:6379/0",
"CDN": {"provider": "cloudflare", "zone_id": "...", "api_token": "..."},
"DEFAULTS": {"s_maxage": 300, "stale_while_revalidate": 3600},
}
# urls.py
from edge_isr import isr, tag
@isr(tags=lambda req, post_id: [tag("post", post_id)], s_maxage=300, swr=3600)
def post_detail(request, post_id):
post = Post.objects.select_related("category").get(pk=post_id)
# Optionally add more tags dynamically
request.edge_isr.add_tags([tag("category", post.category_id)])
return render(request, "post_detail.html", {"post": post})
# models.py
from django.db.models.signals import post_save, post_delete
from edge_isr import revalidate_by_tags, tag
@receiver([post_save, post_delete], sender=Post)
def _post_changed(sender, instance, **kw):
revalidate_by_tags([tag("post", instance.pk), tag("category", instance.category_id)])
That’s it: when a Post changes, the package purges just the affected URLs on your CDN, serves the stale page immediately (SWR), and warms a fresh version in the background.
Concepts
- Tags: strings like
post:42,category:7. Views/fragments declare which tags they depend on. - Tag Graph: Redis sets keep two maps:
tag → {urls}andurl → {tags}. - Revalidation: on data change, determine URLs by tags, purge at CDN (optional), and warm by fetching from origin with a special header.
- SWR headers: responses include
Cache-Control: public, s-maxage=N, stale-while-revalidate=M(and anETagwhen appropriate).
Supported (planned for 0.x series)
- Python: 3.10+
- Django: 4.2, 5.x
- Cache store: Redis (for tag graph & job state)
- Task queue: Celery or RQ (recommended); in-process fallback for dev
- CDN connectors: Cloudflare (0.1), CloudFront (0.2). Works without a CDN too (reverse proxy or just Django cache).
When NOT to use it
- Highly personalized or private pages (vary by cookie/user). Use fine-grained keys or bypass ISR for those routes.
- Endpoints with non-idempotent side effects.
Roadmap
- v0.1: SWR headers, manual tags, Redis tag graph, Cloudflare purge, warmup worker, basic admin.
- v0.2: CloudFront invalidations, automatic tag enrichment helpers, template-fragment decorator.
- v0.3: Admin UX, metrics, per-locale/device cache keys, smarter warmup (rate-limiting, batching).
FAQ
Do I need a CDN? No. You can start locally or behind Nginx/Varnish. CDNs unlock global edge caching and instant purges.
How does it avoid origin stampede? SWR serves the stale version while revalidating once in the background; warmups are queued & throttled.
How do I tag template fragments?
Planned for v0.2. En v0.1, utilisez @isr côté vues (pages complètes). Ci-dessous, API prévue (susceptible d’évoluer) :
Python — décorateur de fragment :
from edge_isr import isr_fragment, tag
@isr_fragment(tags=lambda post: [tag("post", post.id)], s_maxage=300, swr=3600)
def render_post_card(post):
...
Django template — balise de cache de fragment :
{% raw %}
{% isrcache "post_card" tags=["post:{{ post.id }}"] %}
{% include "components/post_card.html" %}
{% endisrcache %}
{% endraw %}
Contributing
Issues and PRs welcome! See docs/contributing.md for setup, test/lint commands, pre-commit hooks, and PR conventions.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_edge_isr-0.0.7.tar.gz.
File metadata
- Download URL: django_edge_isr-0.0.7.tar.gz
- Upload date:
- Size: 12.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eee8a67153f23d7430a5c52c5cec966b8a48ac544b6f31c547080a8a8055c624
|
|
| MD5 |
9a8548b643945c2183db5bea7d8648ae
|
|
| BLAKE2b-256 |
64166dfef90bd0e2eb4d62737d60e9f093bd35d06539c714bd7e886944ef5a4c
|
File details
Details for the file django_edge_isr-0.0.7-py3-none-any.whl.
File metadata
- Download URL: django_edge_isr-0.0.7-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42dd29f7e62354bd293a2222bef8e39a103b11bdceb3a9e2ce1d1bd98800232c
|
|
| MD5 |
f3c2f84d5c64400b70d0fe057a41a83b
|
|
| BLAKE2b-256 |
3bc047e15024a3581aed8d5f4e90663652a4fba2c7ac333159e60fd56f5629d1
|