HTTP-кэширование API: где ломается и что теряет заказчик
HTTP-кэширование — это слой, который стоит между клиентом и бэкендом и решает, можно ли отдать уже готовый ответ, не поднимая приложение. Когда его нет, каждый запрос мобильного клиента, каждая перерисовка интерфейса и каждый повторный вызов из интеграции уходят в базу и пересчитывают один и тот же результат. На среднем API это превращается в рост задержек API: время ответа растёт не линейно, а ступенями, потому что очередь запросов начинает ждать освобождения соединений с базой.
Дальше включается эффект домино. Нагрузка на бэкенд растёт вместе с числом клиентов, а не вместе с объёмом реально новых данных — типичная картина, когда 80% трафика приходится на одни и те же справочники, каталоги и списки. При пиковой нагрузке запас по ресурсам заканчивается раньше, чем приходит основной трафик, и начинаются отказы при пиках: сначала единичные таймауты, потом массовые ошибки на самых «горячих» эндпоинтах, которые как раз и вызываются чаще всего.
Кэширование API снимает эту нагрузку на уровне протокола, а не за счёт добавления серверов. Заголовки ETag и Last-Modified позволяют клиенту спросить «изменилось ли» и получить короткий ответ вместо полного тела, Vary разводит кэш по нужным признакам запроса, а корректные директивы свежести определяют, сколько ответ живёт без обращения к приложению. В результате повторные запросы закрываются на границе, а бэкенд занимается только новыми данными.
Разбираем, где именно ломается HTTP-кэширование в реальных API и как настроить его так, чтобы задержки и отказы перестали быть узким местом.
Что даёт управляемое кэширование: измеримые эффекты
HTTP-кэширование перестаёт быть «настройкой на глаз», когда у каждого ответа есть валидатор: ETag или Last-Modified. Клиент присылает If-None-Match / If-Modified-Since, сервер отвечает 304 Not Modified без тела — время ответа API на повторных запросах падает в разы, потому что по сети едет только заголовок, а не полезная нагрузка. Бэкенд при этом не пересобирает ответ: разгрузка идёт не за счёт кэша на стороне клиента, а за счёт того, что тяжёлая логика вообще не запускается.
Заголовок Vary решает вторую половину задачи — корректное разделение кэша по Accept-Encoding, Authorization и языку. Без него прокси отдаёт одному клиенту ответ, собранный для другого: чужие права, неверную локаль, сжатие не под тот формат. Это и есть отдельный класс отказов — не падение сервиса, а тихая выдача неверных данных, которую ловят уже в проде. Управляемое кэширование убирает его на уровне контракта, а не заплатками в коде.
Стабильность под нагрузкой — следствие. Когда доля повторных запросов, закрытых валидацией, растёт, пиковая нагрузка на бэкенд перестаёт масштабироваться вместе с числом клиентов: те же серверы держат больше трафика, а время ответа остаётся предсказуемым. Разбираем, как это настраивается на практике — от выбора валидатора до политики для прокси и CDN.
Подходы к кэшированию: сравнение режимов и ограничений
Кэширование — это не «включить и забыть», а выбор режима под профиль трафика. Ниже — реальные альтернативы, между которыми приходится выбирать при проектировании API, с их применимостью и ограничениями. Отдельно стоит смотреть на то, кто управляет политикой: приложение, обратный прокси или клиент.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Без кэширования | Персональные данные, платёжные операции, ответы, зависящие от прав пользователя | Каждый запрос идёт до источника, растёт нагрузка и задержка |
Cache-Control: no-store |
Чувствительные ответы, которые нельзя оставлять на диске | Не даёт выигрыша по скорости, только защищает данные |
max-age + public |
Публичные справочники, каталоги, редко меняющийся контент | Устаревание данных в пределах TTL, нужен сброс при обновлении |
ETag + If-None-Match |
Ресурсы, которые меняются нечасто, но должны быть свежими | Лишний round-trip на проверку, ответ 304 вместо тела |
Last-Modified + If-Modified-Since |
Файлы, статика, документы с датой изменения | Точность до секунды, ложные срабатывания при пересохранении |
Vary по заголовкам |
Контент зависит от языка, авторизации, типа устройства | Дробит кэш на много вариантов, растёт объём хранилища |
| Управляемые политики на прокси | Единые правила для десятков сервисов | Требуют согласования с приложением, иначе конфликт заголовков |
Комбинировать режимы приходится почти всегда: ETag для динамики, max-age для статики, Vary там, где ответ зависит от контекста запроса. Ключевой риск — рассогласование заголовков между приложением и прокси: клиент получает устаревший ответ или, наоборот, кэш не работает вовсе.
Выбор подхода фиксируется в управляемых политиках на уровне шлюза, а не в коде каждого сервиса. Так правила остаются предсказуемыми, а ограничения кэширования видны до релиза, а не после инцидента.
Как мы внедряем кэширование: этапы инженерной работы
Кэширование API — это не «включить плагин», а последовательность решений: где стоит кэш, по какому ключу он различает ответы и в какой момент данные считаются устаревшими. Ниже — как мы ведём такую работу, чтобы после запуска поведение было предсказуемым, а не «вроде работает».
- Аудит заголовков. Снимаем реальные ответы API и смотрим, что отдаёт сервер сейчас: есть ли
Cache-Controlс осмысленнымmax-age, проставляется лиETagилиLast-Modified, не теряется лиVaryна заголовках авторизации и языка. Часто выясняется, что часть ответов кэшируется там, где не должна, а часть — наоборот, не кэшируется вовсе. - Проектирование политик. Для каждого типа ресурса определяем режим: публичные справочники — долгий
max-ageсstale-while-revalidate, персональные данные —privateбез разделяемого кэша, изменяемые сущности — валидация черезETagи ответ304. Отдельно фиксируем ключ кэша и набор полей вVary. - Настройка кэша. Прописываем заголовки на уровне приложения и промежуточного слоя, синхронизируем их между собой, чтобы прокси не переписывал политику приложения. Настраиваем корректную работу с условными запросами
If-None-MatchиIf-Modified-Since. - Проверка инвалидации. Прогоняем сценарии изменения данных: обновление записи, смена прав доступа, публикация новой версии. Убеждаемся, что после записи клиент и промежуточный кэш получают свежие данные, а не отдают устаревшие.
- Нагрузочная проверка. Смотрим долю ответов
304и попаданий в кэш, время отклика до и после. Цель — рост доли валидированных ответов и снижение нагрузки на источник. - Передача документации. Отдаём карту политик по эндпоинтам, правила инвалидации и инструкцию для разработчиков: какие заголовки менять можно, а какие сломают кэш.
- Сопровождение. После запуска отслеживаем метрики попаданий и корректируем
max-ageтам, где данные меняются чаще, чем предполагалось.
Опишите вашу задачу — вернёмся с оценкой и планом работ.
Что заказчик получает на руки: конфигурации и регламенты
Работа с HTTP-кэшированием заканчивается не «включили заголовки и забыли», а набором артефактов, которые остаются в репозитории и живут дольше подрядчика. Каждый пункт ниже — то, что можно открыть, проверить и передать новому инженеру без устных пояснений.
- Конфигурации кэша — рабочие файлы для обратного прокси и приложения: правила по
Cache-Control(max-age,s-maxage,stale-while-revalidate), генерацияETagиLast-Modified, настройкаVaryподAccept-Encodingи авторизацию. С комментариями, почему выбран именно такой набор директив для каждого типа ответа. - Схемы потоков — диаграммы пути запроса: клиент → CDN → обратный прокси → приложение → хранилище. На схеме отмечено, на каком слое кэшируется ответ, где происходит ревалидация и какой слой отвечает за инвалидацию.
- Политики инвалидации — регламент сброса: по тегам ресурсов, по префиксам URL, по событию изменения данных. С указанием, что чистится принудительно, а что живёт до истечения
max-age, и как это соотносится с бизнес-логикой. - Документация API — описание эндпоинтов с указанием кэшируемости: какие ответы публичные, какие приватные, где обязательна авторизация и почему такой ответ не должен попадать в общий кэш.
- Регламенты проверки — чек-лист приёмки: контроль заголовков ответа, поведение при
304 Not Modified, корректностьVaryпри разныхAccept-Encoding, отсутствие утечки приватных данных через общий кэш. - Мониторинг и алерты — метрики доли попаданий в кэш, времени ответа на промахе и ревалидации. Пороги, при которых инженер получает уведомление, а не узнаёт о проблеме от пользователей.
- Инструкция по откату — порядок действий, если новая политика кэширования ломает выдачу: как быстро сбросить кэш на всех слоях и вернуть предыдущую конфигурацию.
Пакет самодостаточен: по нему можно провести аудит текущего кэширования, воспроизвести настройки на новом окружении и передать сопровождение другой команде. Опишите задачу — вернёмся с оценкой и планом работ.
Кейс: как управляемое кэширование убрало всплески отказов
Проект — маркетплейс с каталогом на несколько сотен тысяч позиций и мобильным приложением, которое ходило в API за каждым экраном. Кэш-заголовков бэкенд не отдавал: Cache-Control не выставлялся, ETag не считался, ответы уходили с no-store по умолчанию. Каждый запуск приложения и каждый переход по каталогу били в базу напрямую.
Что было до внедрения
На пиках — утренний трафик и распродажи — время ответа каталога уходило за секунду, доля ответов с ошибкой сервера держалась в районе нескольких процентов, а база упиралась в потолок соединений. Отказы шли волнами: перегрузка вызывала таймауты, клиенты повторяли запросы, и нагрузка удваивалась на ровном месте. Дежурная смена тушила это ручным расширением пула и перезапусками.
Что изменили
Настроили управляемое кэширование на уровне API-шлюза и приложения. Для справочников и карточек товаров включили ETag и Last-Modified — повторный запрос без изменений возвращает 304 Not Modified и не доходит до бэкенда. Для списков, зависящих от фильтров, ролей и языка, прописали Vary по нужным заголовкам, чтобы не отдавать чужую версию ответа. Публичные данные получили Cache-Control с раздельными директивами max-age и stale-while-revalidate: клиент отдаёт устаревшую копию, пока фоном подтягивается свежая. Инвалидацию привязали к событиям изменения товара, а не к таймеру.
Что стало
Доля ответов 304 на повторных запросах каталога превысила половину — эти обращения перестали доходить до базы. Время ответа на пиках сократилось в разы, доля ошибок сервера упала до долей процента, а нагрузка на бэкенд в часы пик снизилась примерно вдвое. Волнообразные отказы прекратились: система держит трафик распродажи без ручного вмешательства дежурного.
Практика внедрения показала главное: выигрыш даёт не сам заголовок, а согласованная политика — что кэшируется, на какой срок, по каким признакам различается и когда сбрасывается. Такой подход мы применяем и в других проектах на FastAPI и подобных стеках, где важна предсказуемость под нагрузкой.
Чем отличается ETag от Last-Modified и когда нужен Vary?
ETag и Last-Modified решают одну задачу — дать клиенту дешёвый способ проверить, изменился ли ресурс, не передавая его тело целиком. Last-Modified опирается на время последней правки и работает с точностью до секунды: если два изменения уложились в одну секунду или файл перезаписан тем же содержимым, метка врёт. ETag — это отпечаток конкретной версии представления, поэтому он ловит изменения, которые время не видит, и корректно работает при отдаче из CDN и при сборке ответа из нескольких источников.
На практике их не противопоставляют, а ставят вместе: ETag даёт точную проверку, Last-Modified остаётся запасным валидатором для посредников, которые не умеют работать с отпечатком. Клиент шлёт условный запрос с If-None-Match и If-Modified-Since, сервер сравнивает и возвращает либо тело, либо короткий ответ «не изменилось» — экономия трафика и времени ответа на порядок, а не на проценты.
Слабый ETag отличается от сильного: первый подтверждает смысловую эквивалентность, второй — побайтовую. Для JSON API почти всегда достаточно слабого, для статики и файлов отдачи — сильного. Ошибка новичка — генерировать ETag от времени старта процесса: после перезапуска или переезда на другой узел метка меняется, и весь кэш клиентов инвалидируется разом.
Vary отвечает на другой вопрос — не «та же ли это версия», а «тот же ли это клиент». Если ответ зависит от заголовка Accept, Accept-Encoding, Accept-Language или авторизации, без Vary промежуточный кэш отдаст чужую версию: сжатый ответ — клиенту без поддержки сжатия, английский — тому, кто просил русский. Vary: Accept-Encoding и Vary: Accept-Language закрывают это, но каждый добавленный заголовок дробит кэш на большее число вариантов, поэтому перечислять нужно только то, что реально влияет на тело ответа.
Отдельно стоит Vary: Authorization и приватные ответы: их либо помечают Cache-Control: private, либо запрещают кэширование вовсе, иначе персональные данные осядут в общем кэше. Проверять связку удобно по журналу: доля ответов «не изменилось» на условные запросы показывает, работает ли валидация, а доля попаданий в кэш по каждому варианту Vary — не разрастается ли хранилище.
Частые технические сомнения при настройке кэширования
Кэш чаще всего уже развёрнут — на CDN, reverse-proxy или в самом приложении, и вопрос не «включать ли», а «как не сломать то, что работает». Начинаем с инвентаризации: какие маршруты кэшируются, по каким ключам, кто выставляет заголовки — прокси или бэкенд. Дальше меняем точечно: сначала ответы без персонализации, потом всё остальное, с проверкой на каждом шаге.
Авторизация и кэш конфликтуют там, где ответ зависит от пользователя, а ключ кэша — только от URL. Решение — либо явный Cache-Control: private для таких маршрутов, либо Vary: Authorization, либо разделение на публичный и приватный контуры. Мы не полагаемся на «по умолчанию не закэшируется»: проверяем каждый маршрут, где в ответе есть данные пользователя.
Проверка инвалидации — отдельный этап, а не формальность. Смотрим, что происходит при обновлении ресурса: меняется ли ETag, приходит ли 304 на условный запрос, уходит ли старый ответ с прокси после PURGE. Если инвалидация не срабатывает, клиент получает устаревшие данные и не понимает почему.
У нас уже развёрнут кэш — придётся всё перестраивать?
Нет. Сначала фиксируем текущее поведение: какие заголовки отдаёт бэкенд, что перезаписывает прокси, где расходятся ключи. Перестройка нужна только там, где правила конфликтуют — обычно это 2-3 маршрута из десятков.
Как не сломать авторизацию при включении кэша?
Разделяем контуры: публичные ответы кэшируются по URL, приватные — помечаются private или получают Vary: Authorization. После включения прогоняем сценарии с разными ролями и проверяем, что пользователь не получает чужой ответ.
Как убедиться, что инвалидация работает?
Проверяем цепочку: обновили ресурс — изменился ли ETag или Last-Modified, вернул ли сервер 304 на условный запрос, ушёл ли старый ответ с прокси. Если хотя бы один шаг не срабатывает, кэш отдаёт устаревшее.
Эти сомнения всплывают почти на каждом проекте — и почти всегда решаются точечной настройкой, а не переписыванием. Опишите вашу конфигурацию — вернёмся с оценкой и планом проверки.
Обсудим вашу задачу по кэшированию API
Чтобы вернуться с оценкой и планом внедрения, нам нужен контекст по вашему API: где именно кэширование не работает или работает не так, как ожидается. Опишите задачу в свободной форме — этого достаточно, чтобы наш инженер понял картину и предложил реалистичный план.
Что полезно приложить к описанию:
- текущие заголовки ответов API —
Cache-Control,ETag,Last-Modified,Vary: видно, что уже отдаётся клиентам и где логика рассыпается; - архитектуру: монолит или несколько сервисов, есть ли CDN или обратный прокси перед приложением;
- какие методы и эндпоинты кэшируются, а какие должны оставаться строго свежими (платежи, балансы, статусы заказов);
- как устроена авторизация — токены в заголовке, cookie, сессии: от этого зависит корректная настройка
Varyи приватности кэша; - жалобы от пользователей или метрики: устаревшие данные, лишние запросы, рост нагрузки на базу;
- ограничения: регламенты, требования к безопасности, сроки, которые нельзя сдвигать.
По этим вводным мы вернёмся с оценкой работ и планом внедрения: какие заголовки и правила выставляем, как настраиваем разделение публичного и приватного кэша, как проверяем корректность через условные запросы и откат. Если по ходу разбора появятся развилки — обсудим их с вашим инженером, чтобы решение легло в существующую инфраструктуру, а не потребовало её перестройки.
Опишите задачу — вернёмся с оценкой и планом.







