Cache là cách web tiết kiệm băng thông và tăng tốc lớn nhất, nhưng cũng là nơi gây nhầm lẫn dai dẳng: vì sao đổi CSS rồi mà trình duyệt vẫn hiện bản cũ? vì sao có tài nguyên tải một lần rồi thôi, có cái mỗi lần đều "hỏi" server? Câu trả lời nằm ở chỗ HTTP có hai cơ chế cache tách bạch mà nhiều người trộn làm một: hết hạn (freshness) và xác thực lại (validation). Hiểu rạch ròi hai cái là hết bối rối. Bài này đo thật bằng nginx và curl 7.88.
Hai cơ chế cache
1) Freshness — Cache-Control: server nói "bản này còn tươi trong N giây". Trong thời hạn đó, client dùng thẳng bản đã lưu, không gửi request nào tới server — nhanh nhất có thể.
Cache-Control: max-age=31536000, immutable # tươi 1 năm, không cần xác thực lại
Cache-Control: no-store # không lưu gì (dữ liệu nhạy cảm)
immutable nói thêm "nội dung sẽ không bao giờ đổi" — hợp với asset có hash trong tên (app.abc123.js): khi nội dung đổi, tên file đổi theo nên không cần lo cache cũ.
2) Validation — ETag / Last-Modified: khi bản cache đã hết hạn (hoặc không có max-age), client không tải lại mù quáng mà hỏi có điều kiện: "bản tôi có còn đúng không?". Server đáp 304 Not Modified (không kèm body) nếu chưa đổi, hoặc 200 + nội dung mới nếu đã đổi.
# Response mang dấu vân tay:
ETag: "6abca17e-32" # định danh phiên bản nội dung
Last-Modified: ... GMT # mốc sửa cuối
# Client hỏi lại có điều kiện:
If-None-Match: "6abca17e-32" # theo ETag
If-Modified-Since: ... GMT # theo thời gian

Hình 1: Hai cơ chế cache — Cache-Control/max-age (client không hỏi server) và ETag/Last-Modified + request có điều kiện (server đáp 304 nếu chưa đổi). Xem bằng curl -D -.
Đo thật: 304 Not Modified
Dựng nginx phục vụ file tĩnh (nginx tự sinh ETag và Last-Modified cho mỗi file). Gọi curl và quan sát:

Hình 2: Request đầu trả 200 kèm ETag: "6abca17e-32". Request lại với If-None-Match đúng ETag → 304 Not Modified (không body). Khi file đổi, ETag thành "6abca192-1f", If-None-Match cũ hết khớp → 200 với nội dung mới.
Kết quả xác nhận đúng mô hình:
- Lần đầu:
200 OK+ETag: "6abca17e-32"+Last-Modified+ body 50 byte. - Request lại có điều kiện (
If-None-Match: "6abca17e-32"): server đáp304 Not Modified, không kèm body. Client biết bản cache còn đúng, dùng lại — chỉ tốn vài chục byte header thay vì tải lại toàn bộ.If-Modified-Since(theoLast-Modified) cũng cho304. - Cache-Control theo loại:
/static/app.jstrảpublic, max-age=31536000, immutable(asset có hash, cache mạnh 1 năm);/nostore/me.jsontrảno-store(dữ liệu riêng tư, không cache). - Khi nội dung đổi: sửa file, nginx sinh ETag mới
"6abca192-1f". Request cũ vớiIf-None-Match: "6abca17e-32"giờ hết khớp → server trả200+ bản mới. Cache tự làm mới đúng lúc cần — không bao giờ phục vụ nội dung cũ khi đã đổi.
Đây là câu trả lời cho "vì sao đổi CSS mà trình duyệt vẫn bản cũ": nếu bạn đặt max-age dài mà không đổi tên file (không có hash) và không có bước xác thực, trình duyệt còn trong hạn max-age nên không thèm hỏi server — cứ dùng bản cũ tới khi hết hạn. Cách sửa gốc: hash trong tên file + immutable, hoặc max-age ngắn + ETag.
Đánh đổi và lưu ý
no-cache không phải là "không cache". Tên gây hiểu lầm: Cache-Control: no-cache nghĩa là được lưu, nhưng phải xác thực lại mỗi lần dùng (luôn gửi request có điều kiện). Muốn không lưu gì cả thì dùng no-store. Nhầm hai cái này là lỗi cấu hình phổ biến.
ETag mạnh vs yếu, và bẫy sau CDN/load balancer. nginx sinh ETag từ mtime + kích thước file. Nếu cùng một file được phục vụ từ nhiều máy chủ (mtime lệch nhau), ETag có thể khác nhau giữa các máy → cache miss không đáng có. Khi chạy sau load balancer, cân nhắc chuẩn hóa cách sinh ETag hoặc dựa vào Last-Modified.
Chiến lược thực dụng: asset tĩnh (JS/CSS/ảnh) → hash trong tên + max-age dài + immutable (không bao giờ hỏi lại, đổi thì đổi tên). HTML → no-cache hoặc max-age ngắn + ETag (luôn kiểm tra để lấy bản mới). Dữ liệu riêng tư → no-store. Trộn đúng ba nhóm này là nền của một web nhanh mà vẫn cập nhật.
Ba ý mang về
- Cache HTTP có hai cơ chế tách bạch: freshness (
Cache-Control: max-age— client dùng bản cache, không hỏi server) và validation (ETag/Last-Modified— client hỏi có điều kiện, server đáp304 Not Modifiednếu chưa đổi) — đo thật, request có điều kiện trả 304 không body. - ETag đổi khi nội dung đổi: sửa file → ETag
"6abca17e-32"thành"6abca192-1f"→If-None-Matchcũ hết khớp →200bản mới; đây là cách cache tự làm mới đúng lúc. - Chọn chiến lược theo loại: asset có hash →
max-agedài +immutable; HTML →no-cache/max-age ngắn + ETag; dữ liệu riêng →no-store. Nhớno-cache≠no-store, và ETag có thể lệch sau load balancer.
Nguồn
- MDN — HTTP caching (Cache-Control, ETag, conditional requests): https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
- RFC 9111 (HTTP Caching): https://www.rfc-editor.org/rfc/rfc9111
Phần sau ta sang cách web nhớ bạn giữa các request: cookie và session — Set-Cookie, các thuộc tính HttpOnly/Secure/SameSite, và vì sao chúng quan trọng cho bảo mật.