Что такое ETag и как браузер экономит трафик, ни о чём не спрашивая
Откройте вкладку Network в браузере и обновите любую страницу второй раз — часть запросов вернётся со статусом 304, а не 200, и тело ответа будет пустым. Браузер не спрашивал вас, можно ли сэкономить трафик, — он сам сходил к серверу, показал ему заголовок If-None-Match и получил в ответ короткое «у тебя всё ещё актуальная копия». За этим стоит механизм под названием ETag, и разобраться в нём стоит один раз, а не гадать потом, почему CDN отдаёт «не тот» контент или почему API отвечает медленнее, чем должен.
Содержание
- Что вообще такое ETag и откуда он берётся
- Условный запрос: If-None-Match и 304 Not Modified — как это работает шаг за шагом
- Чем условная проверка отличается от кеширования по времени
- Сильный и слабый ETag: что значит префикс W/
- Как это выглядит на сервере: nginx, статика, API
- Частые грабли: где ETag ломается тихо
Что вообще такое ETag и откуда он берётся
ETag (entity tag) — это заголовок ответа, который сервер добавляет к любому ресурсу: HTML-странице, картинке, JSON из API, файлу стиля. Значение ETag — это своего рода отпечаток конкретного состояния ресурса на момент ответа: хеш содержимого, комбинация размера и времени модификации, номер версии записи в базе — сервер вправе формировать его как угодно, спецификация (RFC 9110) не требует конкретного алгоритма, а требует только одного: если содержимое изменилось, значение ETag должно измениться тоже.
Выглядит это так:
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "5d8c72a3b1f9e4c2a6d0"
Cache-Control: max-age=300
Content-Length: 4213
{...тело ответа...}
Кавычки вокруг значения — не опечатка, а часть синтаксиса заголовка, их обязаны отдавать и сервер, и парсить клиент. Само значение для клиента непрозрачно: браузер не пытается понять, что внутри — хеш SHA, CRC32 от файла или просто числовой counter из базы. Он просто запоминает эту строку как «ярлык» текущего состояния ресурса и в следующий раз предъявит её обратно.
Именно здесь ETag принципиально отличается от простого «кеш по времени». Cache-Control: max-age говорит браузеру «эта копия точно свежая следующие N секунд, вообще не ходи на сервер». А ETag ничего не говорит о том, сколько ресурс будет актуален — он даёт способ спросить сервер «а всё ещё вот это самое состояние?» и получить быстрый ответ без передачи тела целиком. Это два разных вопроса: «сколько ждать» и «изменилось ли». Дальше разберём, как второй механизм устроен по шагам.
Условный запрос: If-None-Match и 304 Not Modified — как это работает шаг за шагом
Механизм называется условным запросом (conditional request), и вот его последовательность:
- Браузер первый раз запрашивает ресурс — обычный GET без условий.
- Сервер отвечает 200 OK, отдаёт тело целиком и добавляет заголовок
ETag: "abc123". - Браузер сохраняет и тело, и значение ETag в своём локальном кеше, привязав их к URL.
- Ресурс нужен снова — кеш браузера решает, что стоит перепроверить актуальность (об этом ниже — не каждый повторный запрос вообще доходит до сети). Браузер отправляет GET, но уже не «пустой», а с заголовком
If-None-Match: "abc123"— это и есть условие: «пришли мне тело, только если текущий ETag НЕ равен этому значению». - Сервер получает запрос, вычисляет актуальный ETag для ресурса и сравнивает со значением из If-None-Match.
- Если совпало — ресурс не менялся, сервер отвечает
304 Not Modifiedс пустым телом и обычно повторяет заголовки кеширования (иногда обновлённый Cache-Control, дату истечения). Тело не передаётся вообще — именно в этом экономия. - Если не совпало — сервер отвечает как обычно, 200 OK с новым телом и новым ETag.
Проверить это руками легко:
# первый запрос — получаем ETag
curl -i https://example.com/style.css | grep -i etag
# ETag: "62a1f3-5d3"
# второй запрос — подставляем полученное значение
curl -i -H 'If-None-Match: "62a1f3-5d3"' https://example.com/style.css
# HTTP/1.1 304 Not Modified
Обратите внимание: 304 — это не «ошибка» и не «редирект», это отдельный, полноценный успешный статус, означающий буквально «твоя локальная копия годится, дальше не ходи». Сервер тратит ресурсы на вычисление и сравнение ETag, но не тратит канал на передачу мегабайтов тела — и именно эта асимметрия (дешёвая проверка вместо дорогой передачи) и есть весь смысл механизма.
Важный нюанс: браузер отправляет If-None-Match самостоятельно, без участия JavaScript или разработчика страницы. Как только браузер один раз увидел ETag на ресурсе, он запоминает его в собственном HTTP-кеше и дальше сам решает, когда и как его предъявлять при повторных обращениях к тому же URL — это встроенное поведение сетевого стека браузера, а не что-то, что нужно явно вызывать из кода страницы.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЧем условная проверка отличается от кеширования по времени
Тут стоит разложить по полочкам, потому что путаница между «кеш истёк» и «ресурс изменился» — источник половины вопросов про ETag.
Cache-Control (и его более старый предшественник Expires) задают временной контракт: max-age=300 значит «следующие 300 секунд просто используй сохранённую копию, вообще не обращайся к серверу — ни обычным запросом, ни условным». Пока max-age не истёк, ETag браузер даже не достаёт — необходимости идти в сеть нет.
ETag вступает в игру после истечения этого TTL. Он не продлевает и не отменяет время жизни кеша — он даёт способ подтвердить, что содержимое всё ещё то же самое, не перекачивая его заново. Разница принципиальна:
| Ситуация | Только Cache-Control | Cache-Control + ETag |
|---|---|---|
| TTL не истёк | Кеш используется, к серверу не ходим | То же самое |
| TTL истёк, ресурс не менялся | Полная перезагрузка тела | Условный запрос → 304 → тело не передаётся |
| TTL истёк, ресурс изменился | Полная перезагрузка тела | Условный запрос → 200 → новое тело |
| Ресурс меняется реже, чем TTL | Лишние полные скачивания после каждого TTL | Экономия трафика на каждой «пустой» проверке |
Из этой таблицы видно главное: ETag особенно выгоден, когда вы не можете позволить себе долгий TTL (например, содержимое обновляется непредсказуемо и вы не хотите отдавать людям устаревшую версию сутки), но при этом реальные изменения происходят редко. Без ETag короткий TTL означает частые полные перекачивания. С ETag короткий TTL означает частые, но дешёвые проверки — а тело едет по сети только тогда, когда действительно есть что передавать.
Отдельно стоит сказать про заголовок Last-Modified и его условную пару If-Modified-Since — это более старый, похожий механизм, работающий по времени модификации файла, а не по хешу содержимого. Он грубее: если файл пересохранили с тем же содержимым (например, скрипт деплоя тронул mtime, а байты не изменились), Last-Modified всё равно скажет «изменилось», и будет лишняя полная передача. ETag, построенный на хеше реального содержимого, в такой ситуации отдаст тот же отпечаток и сервер честно ответит 304. На практике оба заголовка часто присутствуют одновременно ради совместимости, но при наличии обоих браузер и корректно настроенный сервер отдают приоритет ETag как более точному сигналу.
Сильный и слабый ETag: что значит префикс W/
В заголовке ETag можно встретить два формата:
ETag: "5d8c72a3b1f9e4c2a6d0" — сильная валидация (strong)
ETag: W/"5d8c72a3b1f9e4c2a6d0" — слабая валидация (weak), префикс W/
Сильный ETag — это обещание побайтовой идентичности: если значение совпало, содержимое совпадает бит в бит, включая любые метаданные вроде порядка байт при сжатии. Слабый ETag (с префиксом W/) — обещание послабее: содержимое семантически эквивалентно, но байты могут немного отличаться — типичный пример — динамически генерируемая HTML-страница, где сервер гарантирует «то же самое по смыслу», но не берётся гарантировать идентичность до последнего пробела.
Разница важна практически в одном месте: спецификация запрещает использовать слабый ETag там, где нужна точная побайтовая проверка — например, при докачке файла через Range-запросы (If-Range). Для докачки нужен сильный ETag: клиенту важно быть уверенным, что уже скачанная часть и докачиваемая часть — это байты одного и того же состояния файла. Если сервер отдаёт слабый ETag на файл, который может докачиваться, некоторые клиенты и серверы в такой ситуации просто игнорируют Range, скатываясь к полной перезагрузке.
Для статики (CSS, JS, картинки), которая отдаётся файлом как есть, обычно используется сильный ETag, часто на основе inode/размера/времени модификации файла на диске — так делает nginx по умолчанию. Для сгенерированных ответов (API, шаблонизированный HTML) чаще осмысленнее слабый ETag на основе хеша полезной нагрузки, потому что гарантировать побайтовую идентичность генерации по требованию — избыточное и хрупкое обещание.
Как это выглядит на сервере: nginx, статика, API
Если вы уже настраивали кеширование в nginx на VPS, заголовки ETag там, скорее всего, уже включены по умолчанию — но стоит явно проверить их в ответе, а не полагаться на предположение. На nginx для статических файлов ETag включён по умолчанию модулем ядра и обычно строится из mtime и размера файла — вычислять хеш содержимого на каждый запрос nginx не станет, это было бы дорого при большом трафике. Отключить его (если по какой-то причине не нужен) можно так:
location /static/ {
etag off;
}
Оставлять включённым стоит почти всегда — по умолчанию так и есть, дополнительно ничего настраивать не нужно, если вы не написали кастомный обработчик, который сам ставит заголовки ответа и тем самым выключает автоматику nginx.
Важная деталь для сайтов за балансировщиком: если ETag строится из inode и mtime файла на диске, а несколько серверов раздают якобы одну статику из разных копий на разных дисках, значения ETag на них не совпадут, даже при идентичном содержимом — у каждой файловой системы свой inode. Условные запросы будут постоянно «промахиваться» в зависимости от того, на какой бэкенд попал повторный запрос, и экономии не будет вообще. Решения два: либо строить ETag детерминированно (хеш содержимого, а не файловых метаданных), либо закрепить клиента за одним бэкендом через sticky-сессии на балансировщике.
Для API на своём бэкенде (Node.js, Python, PHP — любой стек) ETag нужно формировать и сравнивать вручную, если фреймворк не делает этого сам. Общая логика на псевдокоде:
etag = sha256(response_body) # или хеш от версии записи в БД
if request.headers['If-None-Match'] == etag:
return 304, headers={'ETag': etag}
else:
return 200, body=response_body, headers={'ETag': etag}
Практичный вариант для API, где отдаётся сущность из базы данных — использовать не хеш от сериализованного тела (это лишняя работа CPU на каждый запрос), а поле updated_at или version из самой записи как основу для ETag: тогда сравнение сводится к дешёвому чтению одного поля, а не к пересборке и хешированию всего JSON.
Частые грабли: где ETag ломается тихо
Прокси и CDN переписывают или теряют ETag. Некоторые обратные прокси и CDN по умолчанию не пробрасывают ETag дальше или пересчитывают его сами при сжатии на лету — тогда клиент получает ETag не от origin-сервера, а от прослойки, и совпадение перестаёт что-то значить относительно исходного контента. Если вы настраиваете CDN перед сервером, стоит явно проверить, что заголовок ETag проходит насквозь без модификации, а не генерируется заново на каждом узле CDN — механика того, как устроено кеширование внутри CDN, помогает понять, на каком именно узле это может ломаться.
Gzip/Brotli на лету меняет содержимое ответа, но не всегда меняет ETag. Частый источник багов: сервер сжимает тело динамически, а ETag вычислен от несжатого содержимого раньше — тогда два клиента с разным Accept-Encoding формально получают разные байты под одним ETag, что нарушает саму идею отпечатка. Практика — либо учитывать кодировку в значении ETag (например, суффиксом -gzip), либо полагаться на заголовок Vary для разделения вариантов ответа.
Балансировка без sticky-сессий на кластере с разными ETag на узлах — уже разобрали выше, но стоит повторить: это самая частая причина, почему «ETag вроде настроен, а 304 в логах почти не встречается».
ETag выключен вручную «для простоты» и вместе с ним пропадает экономия. Иногда ETag отключают заодно с отключением всего кеширования при отладке — и забывают включить обратно. Стоит проверять заголовки боевого окружения отдельно от staging.
Путаница между «нет ETag» и «ETag не совпал». Если сервер вообще не отдаёт ETag, браузер не может отправить If-None-Match — условные запросы просто не происходят, и вся экономия трафика откатывается к тому, что решает Cache-Control в одиночку. Это не поломка, а осознанный выбор конфигурации, но стоит понимать: без ETag после истечения TTL всегда будет полная перезагрузка тела, даже если содержимое не изменилось ни на байт.
Ошибочное ощущение, что ETag — это защита или версионирование. ETag — отпечаток состояния для HTTP-кеша, а не механизм оптимистичной блокировки и не гарантия целостности данных. Значение легко подделывается клиентом (это просто текст в заголовке запроса), поэтому полагаться на If-None-Match как на защиту от гонок записи в бизнес-логике не стоит — для этого есть отдельные механизмы уровня приложения и базы (например, заголовок If-Match для optimistic concurrency в некоторых REST API — но это осознанное расширение идеи, а не автоматическое поведение браузера).
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Экономит ли ETag трафик, если ресурс меняется на каждом запросе?
Нет. Если содержимое действительно разное каждый раз, ETag тоже будет каждый раз новым, сервер ответит 200 с полным телом — условная проверка просто добавит один лишний заголовок к запросу без выигрыша. ETag выгоден именно тогда, когда содержимое меняется реже, чем к нему обращаются.
Можно ли использовать ETag вместо Cache-Control?
Можно, но обычно не стоит: без Cache-Control браузер будет ходить на сервер при каждом обращении к ресурсу (пусть и с условным запросом, а не полной загрузкой) — а с разумным max-age он вообще не пойдёт в сеть, пока TTL не истёк, что дешевле любого 304. Оба заголовка решают разные задачи и хорошо работают вместе.
Почему в DevTools я вижу 200 из кеша (disk cache / memory cache), а не 304?
Это как раз работа Cache-Control без сети: если TTL ещё не истёк, браузер вообще не формирует HTTP-запрос, а отдаёт копию из локального хранилища — статус 200 в этом случае показывается условно, для удобства чтения, реального обращения к серверу не было. 304 вы увидите только после истечения TTL, когда браузер решил перепроверить актуальность.
Нужно ли вручную удалять ETag при деплое новой версии статики?
Нет, если файлы действительно меняются — новое содержимое даст новый ETag автоматически (новый хеш или новые mtime/размер файла). Проблема с «залипшим» старым контентом почти всегда не в ETag, а в слишком долгом Cache-Control на промежуточном CDN или прокси, который вообще не доходит до condition-проверки, пока не истечёт его собственный TTL.
Работает ли ETag для API-ответов в формате JSON так же, как для статики?
Да, механизм универсален для любого HTTP-ответа. Разница только в том, что для статики ETag почти всегда считает веб-сервер автоматически, а для динамических API-ответов формирование и сравнение ETag обычно нужно реализовать в коде приложения самостоятельно.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →