Кеш страниц в Django: чек-лист перед включением
Кеш страниц в Django ускоряет отдачу в разы, но легко раздаёт чужие корзины и залогинивает не тех. Чек-лист того, что проверить до включения per-view и per-site кеша.

Кеширование целых страниц в Django выглядит как бесплатный ускоритель: обернул view декоратором — и ответ стал отдаваться за миллисекунды вместо сотен. Проблема в том, что кеш страниц не знает, что именно вы кешируете. Он одинаково охотно сохранит и статичный лендинг, и страницу с приветствием «Здравствуйте, Иван» — а потом покажет это приветствие всем подряд. Этот чек-лист — про то, что проверить до того, как @cache_page уедет в прод. Актуально для Django 4.2 LTS и 5.x, поведение бэкендов кеша между этими версиями не менялось.
Три уровня кеша страниц и что из них опасно
Django даёт кеширование страниц на трёх уровнях, и путаница между ними — источник большинства инцидентов.
- Per-site cache — middleware
UpdateCacheMiddlewareиFetchFromCacheMiddleware, кеширует все GET/HEAD-ответы сайта целиком. - Per-view cache — декоратор
cache_page(timeout)на конкретной view. - Template fragment cache — тег
{% cache %}внутри шаблона, кеширует только кусок.
Опасность растёт снизу вверх. Фрагмент кеширует ровно то, что вы ему отдали, и вы сами задаёте ключ. Per-view уже кеширует ответ view вместе с любыми персональными данными, которые туда попали. Per-site кеширует весь сайт и почти всегда — самая частая причина утечки чужих сессий на публичных проектах.
Почему это ломается именно на аутентификации
Кеш страницы хранит готовый HTTP-ответ по ключу, который Django строит из метода, пути и заголовков из Vary. Если в Vary нет Cookie, то запрос залогиненного пользователя и запрос анонима дают один и тот же ключ. Кто первым положил ответ в кеш — тем и увидят остальные.
Кеш страниц не различает пользователей сам по себе. Различие появляется только там, где вы явно сказали ему различать — через Vary. Всё остальное он честно раздаёт всем одинаково.
Чек-лист перед включением
- На странице точно нет персональных данных? Имя пользователя в шапке, содержимое корзины, CSRF-зависимый контент — всё это делает страницу непригодной для per-view и per-site кеша без разделения по пользователю. Открывайте кеш только на реально общих страницах: каталог без персонализации, статьи, документация.
- Стоит ли
Vary: Cookie, если контент зависит от сессии? Декораторvary_on_cookieдобавит cookie в ключ кеша. Но помните: если у каждого пользователя своя сессионная кука, кеш фактически перестаёт переиспользоваться между людьми — вы кешируете отдельно для каждого. Иногда это осмысленно, иногда сводит выгоду к нулю. - Метод только GET/HEAD?
cache_pageкеширует только безопасные методы. POST проходит мимо. Проверьте, что вы не завернули в кеш view, которая обрабатывает и форму, и её отображение — GET-ответ закешируется, и пользователь будет видеть устаревшее состояние формы. - CSRF-токен не попадёт в кеш? Если в закешированном HTML есть
{% csrf_token %}, все получат один и тот же токен. Для анонимных страниц это обычно не критично, но формы за логином так кешировать нельзя. - Какой backend кеша? На
LocMemCache(по умолчанию) кеш живёт в памяти процесса. При нескольких воркерах Gunicorn у каждого свой кеш, и попадания непредсказуемы. Для страниц нужен общий backend — Redis или Memcached. - Настроен ли timeout осознанно? Значение по умолчанию берётся из
CACHES['default']['TIMEOUT'], если не передать явно. 300 секунд для новостной ленты — это до пяти минут показа устаревших данных. - Есть ли план инвалидации?
cache_pageсам ничего не сбрасывает при изменении данных. Либо ждёте истечения timeout, либо чистите ключи вручную. Инвалидация — отдельная большая тема, здесь только фиксируем, что она нужна.
Как это выглядит в коде
Минимально безопасный вариант для публичной страницы каталога, которая одинакова для всех:
# Django 4.2 / 5.x
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from django.shortcuts import render
# Кешируем на 10 минут. Ответ одинаков для всех анонимов,
# персональных данных на странице нет.
@cache_page(60 * 10)
def public_catalog(request):
products = Product.objects.filter(is_active=True)
return render(request, "catalog.html", {"products": products})
Если контент всё же зависит от языка или другого заголовка — разделяем ключ явно, чтобы англоязычная версия не улетела русскоязычному:
@cache_page(60 * 10)
@vary_on_headers("Accept-Language")
def public_catalog(request):
...
Для куска шаблона, который дорого рендерить, но который общий для всех, — фрагментный кеш вместо кеша всей страницы:
{% load cache %}
{# ключ включает переменную region, поэтому для разных регионов #}
{# будет свой закешированный блок #}
{% cache 600 sidebar_top region.id %}
{% include "partials/heavy_sidebar.html" %}
{% endcache %}
Сравнение уровней: где что применять
| Уровень | Область | Контроль над ключом | Риск утечки данных | Когда брать |
|---|---|---|---|---|
| Per-site (middleware) | весь сайт | низкий | высокий | простой сайт без личных кабинетов |
Per-view (cache_page) | отдельная view | средний, через Vary | средний | публичные страницы, каталоги, статьи |
Fragment ({% cache %}) | кусок шаблона | высокий, ключ ваш | низкий | дорогие общие блоки на персональных страницах |
Если не сработало
- Кеш вообще не попадает, каждый запрос считается заново. Проверьте backend: на
LocMemCacheпри нескольких воркерах попадания редки. Переключитесь на Redis/Memcached и убедитесь, что все процессы ходят в один инстанс. - Пользователи видят чужие данные. На странице есть персональный контент, а
Vary: Cookieне выставлен. Уберите кеш с этой view либо разделите ключ по пользователю — но тогда взвесьте, есть ли смысл в кеше вообще. - Страница не обновляется после изменения данных. Это ожидаемо:
cache_pageдержит ответ до истечения timeout. Уменьшите timeout или добавьте явную инвалидацию нужных ключей. - Форма показывает старое состояние или ошибку CSRF. GET-ответ с формой закеширован. Не кешируйте view, которая одновременно рисует и обрабатывает форму, либо выносите форму во фрагмент вне кеша.
- Middleware-кеш игнорирует ваши декораторы. Порядок middleware важен:
UpdateCacheMiddlewareидёт первым в списке,FetchFromCacheMiddleware— последним. Перепутанный порядок ломает логику.
Что настроить дальше
После включения кеша страниц смотрите на два показателя в проде: hit rate самого кеша (Redis/Memcached отдают статистику попаданий) и долю ответов с заголовком Cache-Control, которую вы реально отдаёте. Если hit rate низкий при выставленном Vary: Cookie — вы кешируете персонально и почти впустую, стоит перейти на фрагментный кеш. Следующий шаг после этого — инвалидация по сигналам моделей, чтобы не жить на голом timeout. Точный список параметров backend'а сверяйте в разделе документации Django по кешированию для вашей версии.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Django documentation: Django's cache framework
Частые вопросы
У меня Django 3.2, а не 4.2 — чек-лист подойдёт?
Можно ли кешировать страницу личного кабинета?
Почему кеш не работает, хотя декоратор стоит?
Что будет, если я забуду про инвалидацию?
Как кеш страниц дружит с CSRF-токеном?
cache_page или {% cache %} в шаблоне — что выбрать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.