Reducing Preflight Frequency with Header Caching: Configuration & Validation

Directs engineers on configuring Access-Control-Max-Age to minimize redundant OPTIONS requests. This guide details browser-specific cache limits, exact failure modes, and step-by-step validation procedures.

Key Implementation Points:

Browser Preflight Cache Limits & Engine Caps

Rendering engines parse Access-Control-Max-Age according to strict WHATWG Fetch Standard rules. Servers cannot override these client-side enforcement boundaries.

Engine Maximum Cache Duration Behavior on Exceeding Value
Chromium (Chrome/Edge) 86400 seconds (24 hours) Silently truncates to cap
Firefox (Gecko) 86400 seconds (24 hours) Silently truncates to cap
WebKit (Safari/iOS) 600 seconds (10 minutes) Silently truncates to cap

Values exceeding these limits are discarded without console warnings. For broader architectural context on cache layering, consult Preflight Request Optimization & Caching Strategies.

Server-Side Header Configuration & Injection

The Access-Control-Max-Age header must be injected exclusively into OPTIONS responses. Attaching it to standard GET or POST endpoints yields zero caching benefit.

Configuration Requirements:

Nginx Configuration (Strict OPTIONS Caching)

location /api/ {
  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT';
    add_header 'Access-Control-Max-Age' '86400';
    add_header 'Content-Length' '0';
    add_header 'Content-Type' 'text/plain';
    return 204;
  }
}

Express.js Middleware (Dynamic TTL)

app.options('/api/*', (req, res) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH');
  res.header('Access-Control-Max-Age', process.env.NODE_ENV === 'production' ? '86400' : '3600');
  res.sendStatus(204);
});

Root Cause Analysis: Cache Invalidation Triggers

Preflight caching fails when request signatures deviate from the cached OPTIONS response. Browsers enforce strict isolation rules that override Max-Age directives.

Common Invalidation Vectors:

Client-Side Validation Script

async function testPreflightCache() {
  const start = performance.now();
  await fetch('https://api.example.com/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'same-origin'
  });
  const duration = performance.now() - start;
  console.log(`Request latency: ${duration}ms (preflight cached if < 50ms)`);
}

Step-by-Step Validation & Network Audit

Verify cache enforcement using browser DevTools and CLI utilities. Manual inspection confirms TTL alignment and detects silent failures.

Validation Workflow:

  1. Open DevTools Network tab and filter by OPTIONS.
  2. Inspect the Timing tab for (disk cache) or (memory cache) indicators.
  3. Execute raw header validation via CLI to bypass browser abstraction layers.
  4. Cross-reference response headers with browser cache storage to confirm TTL alignment.
  5. Monitor repeated preflights post-deployment to detect accidental cache busting.

CLI Validation Command

curl -I -X OPTIONS \
  -H 'Origin: https://client.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: Content-Type' \
  https://api.example.com/data

Expected Output Verification:

HTTP/2 204
access-control-allow-origin: https://client.example.com
access-control-allow-methods: GET, POST, PUT
access-control-max-age: 86400
content-length: 0

Common Configuration Mistakes

Issue Technical Explanation
Applying Access-Control-Max-Age to GET/POST responses Browsers only cache preflight responses when attached to OPTIONS 204/200. Resource headers have zero effect on preflight frequency.
Expecting 30-day cache durations in Safari WebKit hard-codes a 10-minute maximum. Values above 600s are silently truncated, causing preflight storms on iOS/macOS.
Using withCredentials: true with caching Credential-enabled requests deliberately bypass preflight cache for strict origin isolation, forcing a new OPTIONS per session.

Frequently Asked Questions

Does Access-Control-Max-Age work with withCredentials: true?

No. Browsers intentionally bypass preflight caching when credentials are enabled to enforce strict origin isolation and prevent credential leakage.

Why does Safari ignore my 24-hour preflight cache?

WebKit enforces a hard 10-minute (600s) cap on preflight cache duration. Values exceeding this are truncated silently without console warnings.

How do I force a preflight cache refresh during deployment?

Change the Access-Control-Allow-Methods or Access-Control-Allow-Headers values. Alternatively, deploy a cache-busting query parameter to the OPTIONS endpoint to invalidate existing browser caches.