If a Django view runs many queries or heavy computation but its data only changes a few times a day, wrap it with @cache_page(60 * 15). Django stores the whole response in the cache and serves it unchanged for 15 minutes, instead of running the view again on every request. This fits list pages, stats pages, and rarely changing detail pages — but never apply it blindly to pages that hold per-user data.
The problem
Imagine your stats page runs a batch of aggregate queries every time it opens, then renders the same table of numbers for every visitor. The more traffic you get, the more load the database takes for results that are nearly identical:
# views.py — without cache: every request reruns everythingfrom django.shortcuts import renderfrom django.db.models import Sumfrom .models import Orderdef thong_ke(request): don_hang = Order.objects.filter(trang_thai="hoan_thanh") context = { "tong_doanh_thu": don_hang.aggregate(tong=Sum("tong_tien"))["tong"], "so_don": don_hang.count(), } return render(request, "thong_ke.html", context)
The problem is not the query itself, but that it repeats pointlessly: day-based stats are still correct a few minutes later, yet the server recomputes them from scratch for every viewer.
The solution
One decorator is all it takes — Django handles the rest:
# views.py — computed only once every 15 minutesfrom django.views.decorators.cache import cache_pagefrom django.shortcuts import renderfrom django.db.models import Sumfrom .models import Order@cache_page(60 * 15)def thong_ke(request): don_hang = Order.objects.filter(trang_thai="hoan_thanh") context = { "tong_doanh_thu": don_hang.aggregate(tong=Sum("tong_tien"))["tong"], "so_don": don_hang.count(), } return render(request, "thong_ke.html", context)
First, declare a cache backend in settings.py. In-memory caching is enough while developing; switch to Redis in production:
# settings.py — dev environmentCACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", }}
If you do not want to touch the view, wrap it in urls.py instead — handy when the view lives in a third-party app:
# urls.pyfrom django.urls import pathfrom django.views.decorators.cache import cache_pagefrom . import viewsurlpatterns = [ path("thong-ke/", cache_page(60 * 15)(views.thong_ke), name="thong_ke"),]
Three things to remember. First, cache_page distinguishes by the full URL, including the query string, so ?page=2 is a separate cached copy — pagination still works correctly. Second, the cache does not know the data changed: within 15 minutes, a new order still will not show up on the page. So pick the timeout to match how often the data changes, or change key_prefix to “flush” the old cache. Third, a page personalized by cookie must also add @vary_on_cookie, and the decorator order is mandatory: @cache_page on top, @vary_on_cookie below. Reverse the order and the next user may receive the previous user’s cached page — a bug I verified directly before writing this post.
# CORRECT order: cache_page on top, vary_on_cookie belowfrom django.views.decorators.cache import cache_pagefrom django.views.decorators.vary import vary_on_cookie@cache_page(60 * 15)@vary_on_cookiedef trang_ca_nhan(request): return render(request, "trang_ca_nhan.html", {"user": request.user})
Apply it to your project
Start with the slowest page whose data rarely changes: a product list or a stats table. Measure with django-debug-toolbar before and after adding the decorator to see the query count drop to 0 on cache hits. Try a 5–15 minute timeout first, watch it for a day, then extend it. And never cache the cart or a personal dashboard — where each person must see their own data, a wrong cache is worse than no cache.
Related posts
- Django: cache kết quả truy vấn bằng cache framework trong 5 phút — khỏi truy vấn lại mỗi request
- Django: lấy hàng loạt object theo danh sách ID bằng in_bulk() trong 5 phút
- Django: Group Stats with values() + annotate() — GROUP BY in a Single Query
Frequently asked questions
How is cache_page different from cache.get/cache.set?
cache_page caches the entire response by URL, with no manual read/write cache logic. cache.get/cache.set are more flexible: they cache individual pieces of data or querysets and can be used anywhere.
How long should the timeout be?
Match it to how often the data changes. Data that changes daily works well with 15–60 minutes; data that changes every few minutes should only be cached for 1–5 minutes, or not cached at all — optimize the query first instead.
Why does the page still show old content after I save new data?
Because the cache has not expired yet. Fix it by waiting out the timeout, setting a shorter timeout, or changing key_prefix so the old cache is ignored immediately.
Can I cache a page that requires login?
Yes, but you must attach @vary_on_cookie in the correct order as shown in the solution section, so each user gets their own cached copy. If you are not sure, the safest choice is not to cache personal pages.
💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold the best ideas into the post.
Sources
- Django documentation — Django’s cache framework: https://docs.djangoproject.com/en/5.2/topics/cache/
