HTTP Caching (ETags, Cache-Control)
debt(d5/e3/b5/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints.tools list includes lighthouse, curl, and webpagetest — all specialist tools that must be deliberately run to surface missing or misconfigured Cache-Control headers. Misconfigured caching is not caught by compilers or default linters; it requires running a performance audit or inspecting HTTP response headers manually.
Closest to 'simple parameterised fix' (e3). The quick_fix describes a pattern-replacement approach: assign the correct Cache-Control directive based on asset type (hashed static → immutable, authenticated pages → no-store, dynamic public content → ETag). This is more than a single-line patch because it requires categorising different response types and applying the right header to each, but it stays within one configuration layer (server config, middleware, or CDN rules) without touching application logic broadly.
Closest to 'persistent productivity tax' (b5). HTTP caching applies across all web and API contexts (applies_to: web, api). Every future feature that serves a new resource type forces a decision about its caching policy. Wrong choices (e.g., no-cache on static assets, or caching user-specific data) create ongoing performance or correctness problems across many work streams. It's not as gravity-defining as an ORM choice (b7+), but it imposes a recurring correctness tax on all response-serving code.
Closest to 'serious trap' (t7). The misconception field explicitly states the canonical wrong belief: developers assume Cache-Control: no-cache means 'never cache,' when it actually means 'revalidate before use.' The directive that truly prevents caching is no-store. This contradicts the plain-English reading of the directive name and contradicts how developers familiar with other caching systems (e.g., application-level caches) expect 'no-cache' to behave, making it a serious semantic trap.
Also Known As
TL;DR
Explanation
HTTP caching is controlled primarily by Cache-Control (max-age, no-cache, no-store, public/private), ETags (opaque hash of response content enabling conditional requests), and Last-Modified. Correct caching headers can eliminate round-trips entirely for static assets and reduce server load for API responses. In PHP, set cache headers explicitly with header() — framework responses typically provide helper methods. For dynamic content, use Vary headers to cache different representations correctly and implement conditional GET responses that return 304 Not Modified without a body.
Common Misconception
Why It Matters
Common Mistakes
- No Cache-Control header — browsers guess, often defaulting to no caching.
- Cache-Control: no-cache on truly static assets — forces revalidation on every request.
- Not using ETags or Last-Modified for dynamic content that rarely changes.
- Caching responses that contain user-specific data without Vary: Cookie or private directive.
Code Examples
// No cache headers — browser re-fetches on every navigation:
header('Content-Type: application/json');
echo json_encode($data);
// Should add: header('Cache-Control: public, max-age=300');
// Or for user data: header('Cache-Control: private, max-age=60');
// ETags — conditional GET, saves bandwidth
public function show(int $id): Response {
$user = $this->users->find($id);
$etag = md5($user->updatedAt->format('U'));
if ($this->request->header('If-None-Match') === $etag) {
return response('', 304); // Not Modified — no body sent
}
return response()->json($user)
->header('ETag', $etag)
->header('Cache-Control', 'private, must-revalidate');
}
// Static assets — immutable long cache
header('Cache-Control: public, max-age=31536000, immutable');
// API responses — short cache, must revalidate
header('Cache-Control: private, max-age=60, must-revalidate');