HTTP

Header

Access-Control-Max-Age Header

Learn how Access-Control-Max-Age specifies how long browsers can cache CORS preflight results. Reduce preflight requests and improve cross-origin performance.

5 min read intermediate Try in Playground

TL;DR: Specifies how long browsers can cache CORS preflight results (in seconds). Reduces preflight requests for better performance.

What is Access-Control-Max-Age?

The Access-Control-Max-Age header tells browsers how long (in seconds) they can cache the results of a CORS preflight request. This improves performance by reducing the number of preflight OPTIONS requests needed for cross-origin requests.

Think of it like a “remember this permission for X seconds” instruction. Instead of asking for permission every time, the browser can remember the answer and skip the preflight check for subsequent requests.

How Access-Control-Max-Age Works

First Request - Preflight:

OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
```http

**Server Response - Cache for 1 hour:**

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600

Subsequent Requests - Skip Preflight:

POST /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json

{"name": "John"}
```text

For the next hour, the browser skips the preflight and directly sends the POST request.

## Syntax

```http
Access-Control-Max-Age: <delta-seconds>

The value is the number of seconds the preflight response can be cached.

# Cache for 5 minutes
Access-Control-Max-Age: 300

# Cache for 1 hour
Access-Control-Max-Age: 3600

# Cache for 24 hours
Access-Control-Max-Age: 86400

# Don't cache (always preflight)
Access-Control-Max-Age: 0

# Don't cache (header omitted)
# (no Access-Control-Max-Age header)
```http

## Common Examples

### Short Cache Duration (5 minutes)

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 300

Good for APIs where permissions might change frequently.

Medium Cache Duration (1 hour)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600
```http

Balanced approach for most applications.

### Long Cache Duration (24 hours)

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
Access-Control-Max-Age: 86400

Good for stable APIs with consistent CORS policies.

No Caching (Development)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 0
```text

Useful during development when CORS configuration might change frequently.

## Real-World Scenarios

### Production API - Optimal Performance

**Server configuration:**

```javascript
// Node.js/Express
app.options('/api/*', (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.setHeader('Access-Control-Max-Age', '7200') // 2 hours
  res.sendStatus(204)
})

Impact:

  • First request: Preflight + actual request = 2 round trips
  • Next 2 hours: No preflight, just actual request = 1 round trip
  • Performance improvement: 50% reduction in requests

Development Environment - No Caching

// Development configuration
const isDevelopment = process.env.NODE_ENV === 'development'

app.options('/api/*', (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.setHeader('Access-Control-Max-Age', isDevelopment ? '0' : '3600')
  res.sendStatus(204)
})
```text

### High-Traffic API - Aggressive Caching

```javascript
// High-traffic public API
app.options('/api/public/*', (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
  res.setHeader('Access-Control-Max-Age', '86400') // 24 hours
  res.sendStatus(204)
})

Dynamic CORS with Moderate Caching

app.options('/api/*', (req, res) => {
  const allowedOrigins = [
    'https://app.example.com',
    'https://admin.example.com',
    'https://mobile.example.com'
  ]

  const origin = req.headers.origin

  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    res.setHeader('Access-Control-Max-Age', '1800') // 30 minutes
    res.sendStatus(204)
  } else {
    res.sendStatus(403)
  }
})
```text

## Browser Limitations

Different browsers have different maximum cache times:

- **Firefox**: Maximum 24 hours (86400 seconds)
- **Chrome/Chromium**: Maximum 2 hours (7200 seconds) - older versions; newer versions: 2 hours default, but can go up to 24 hours
- **Safari**: Maximum 24 hours (86400 seconds)

```javascript
// Safe cross-browser value
res.setHeader('Access-Control-Max-Age', '7200') // 2 hours

Performance Impact

Without Caching (Max-Age: 0)

Request 1: OPTIONS + POST = 2 requests
Request 2: OPTIONS + POST = 2 requests
Request 3: OPTIONS + POST = 2 requests
Total: 6 requests

With Caching (Max-Age: 3600)

Request 1 (0:00):  OPTIONS + POST = 2 requests
Request 2 (0:05):  POST only = 1 request (cached)
Request 3 (0:10):  POST only = 1 request (cached)
Request 4 (1:05):  OPTIONS + POST = 2 requests (cache expired)
Total: 5 requests (16% reduction)

High Frequency (100 requests/hour)

Without caching: 200 requests
With Max-Age: 3600: 102 requests (49% reduction)
With Max-Age: 7200: 101 requests (49.5% reduction)

Best Practices

1. Choose Appropriate Cache Duration

// ❌ Too short - unnecessary preflights
res.setHeader('Access-Control-Max-Age', '30') // 30 seconds

// ❌ Too long - can't quickly update CORS policy
res.setHeader('Access-Control-Max-Age', '604800') // 7 days

// ✅ Balanced - good performance, reasonable update window
res.setHeader('Access-Control-Max-Age', '3600') // 1 hour
```javascript

### 2. Environment-Specific Configuration

```javascript
const maxAge = {
  development: 0, // No cache - easy debugging
  staging: 1800, // 30 minutes - moderate cache
  production: 7200 // 2 hours - optimal performance
}

res.setHeader('Access-Control-Max-Age', maxAge[process.env.NODE_ENV])

3. Consider Your Update Frequency

// Stable API - long cache
app.options('/api/v1/*', (req, res) => {
  res.setHeader('Access-Control-Max-Age', '86400') // 24 hours
  // ... other headers
  res.sendStatus(204)
})

// Experimental API - short cache
app.options('/api/beta/*', (req, res) => {
  res.setHeader('Access-Control-Max-Age', '300') // 5 minutes
  // ... other headers
  res.sendStatus(204)
})
```text

### 4. Complete Preflight Response

```javascript
app.options('/api/*', (req, res) => {
  // All necessary CORS headers together
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
  res.setHeader('Access-Control-Max-Age', '3600')
  res.setHeader('Access-Control-Allow-Credentials', 'true')
  res.sendStatus(204)
})

Testing

Check Preflight Caching

# First request - should include preflight
curl -X OPTIONS https://api.example.com/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -v

# Look for:
# Access-Control-Max-Age: 3600
```text

### Verify Cache Duration

```javascript
// Browser console
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Test' })
})

// Check Network tab:
// First request: OPTIONS + POST
// Subsequent requests: POST only (if within cache period)

Test Different Values

# Test short cache
curl -X OPTIONS https://api.example.com/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -v | grep "Access-Control-Max-Age"

# Expected: Access-Control-Max-Age: 3600
```javascript

## Common Patterns

### Progressive Enhancement

```javascript
// Start conservative, increase as stable
const appAge = Date.now() - appStartTime
const maxAge = appAge < 3600000 ? 300 : 3600 // 5 min 1 hour

res.setHeader('Access-Control-Max-Age', String(maxAge))

Per-Endpoint Configuration

const corsConfig = {
  '/api/public': { maxAge: 86400 }, // 24 hours
  '/api/auth': { maxAge: 1800 }, // 30 minutes
  '/api/admin': { maxAge: 300 } // 5 minutes
}

app.options('/api/*', (req, res) => {
  const config = corsConfig[req.path] || { maxAge: 3600 }
  res.setHeader('Access-Control-Max-Age', String(config.maxAge))
  // ... other headers
  res.sendStatus(204)
})

Frequently Asked Questions

What is Access-Control-Max-Age?

This header specifies how long browsers can cache preflight results in seconds. It reduces preflight requests by reusing cached CORS permissions.

What is a good max-age value?

Common values are 86400 (1 day) or 7200 (2 hours). Browsers cap this: Chrome at 2 hours, Firefox at 24 hours. Balance between performance and flexibility to change CORS policy.

Does max-age affect all CORS requests?

No, only preflight (OPTIONS) results are cached. Simple requests that do not trigger preflight are not affected by this header.

How do I clear the preflight cache?

Users can clear browser cache. Developers can use different URLs or wait for expiration. During development, use short max-age or disable cache in DevTools.

Keep Learning