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

Ảnh chụp đoạn mã nền tối giải thích cache HTTP hai cơ chế hết hạn và xác thực lại, một freshness Cache-Control không hỏi server chút nào Cache-Control max-age 31536000 immutable trong thời hạn giây client dùng bản cache 0 request mạng immutable nội dung không đổi đừng cả xác thực lại hợp với asset có hash trong tên app.abc123.js Cache-Control no-store không lưu gì cả dữ liệu nhạy cảm, hai validation ETag Last-Modified hỏi còn mới không response mang dấu vân tay của nội dung ETag 6abca17e-32 hash định danh phiên bản Last-Modified mốc sửa lần cuối lần sau client hỏi có điều kiện If-None-Match theo ETag If-Modified-Since theo thời gian chưa đổi 304 Not Modified không body tiết kiệm băng thông, xem bằng curl -D in header response curl -H If-None-Match request có điều kiện curl -o -w http_code xem 200 hay 304

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:

Ảnh chụp bảng kết quả đo thật nền tối chạy curl 7.88 và nginx, phần một request lần đầu 200 cộng ETag cộng Last-Modified HTTP/1.1 200 OK Content-Length 50 Last-Modified Wed 30 Sep 2026 05:43:26 GMT ETag 6abca17e-32, phần hai request lại có điều kiện ra 304 không body curl -H If-None-Match 6abca17e-32 ra HTTP/1.1 304 Not Modified không Content-Length body curl -H If-Modified-Since ra HTTP 304, phần ba Cache-Control theo loại tài nguyên slash static slash app.js ra public max-age 31536000 immutable slash nostore slash me.json ra no-store, phần bốn sửa file ETag đổi If-None-Match cũ hết khớp ra 200 ETag cũ 6abca17e-32 ra ETag mới 6abca192-1f curl -H If-None-Match 6abca17e-32 ra HTTP 200 nội dung đã đổi server gửi bản mới cache tự làm mới

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 đáp 304 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 (theo Last-Modified) cũng cho 304.
  • Cache-Control theo loại: /static/app.js trả public, max-age=31536000, immutable (asset có hash, cache mạnh 1 năm); /nostore/me.json trả 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ới If-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ề

  1. 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 đáp 304 Not Modified nếu chưa đổi) — đo thật, request có điều kiện trả 304 không body.
  2. ETag đổi khi nội dung đổi: sửa file → ETag "6abca17e-32" thành "6abca192-1f" → If-None-Match cũ hết khớp → 200 bản mới; đây là cách cache tự làm mới đúng lúc.
  3. Chọn chiến lược theo loại: asset có hash → max-age dà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

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.