Skip to main content
Cache-Control is an HTTP response header that controls how long browsers and CDNs can cache responses. Sonamu makes it easy to set Cache-Control headers on API and SSR responses.

What is HTTP Caching?

HTTP caching is a mechanism that improves performance by reusing responses for identical requests.

Without Caching

Problem: Queries the DB for the same data every time (slow, server load)

With Caching

Benefits: Instant response without server request (fast, reduced server load)

Cache-Control Header

Basic Structure

Cache-Control: public, max-age=3600
Meaning:
  • public: Can be stored by all caches (browser, CDN)
  • max-age=3600: Valid for 3600 seconds (1 hour)

Key Directives

public vs private
Cache-Control: public, max-age=3600
  • public: Can be stored by all caches (browser, CDN, proxy)
  • Use case: Same response for all users (product lists, announcements)
Cache-Control: private, max-age=3600
  • private: Can only be stored in browser (not CDN/proxy)
  • Use case: Different response per user (my page, shopping cart)

Stale-While-Revalidate

A strategy that returns expired cache (stale) immediately while updating in the background.
Cache-Control: public, max-age=60, stale-while-revalidate=300

How It Works

Benefits:
  • User always gets fast response (returns stale immediately)
  • Background update ensures fresh data for next user
CloudFront Support: AWS CloudFront supports stale-while-revalidate.

Stale-If-Error

Uses stale cache when errors occur.
Cache-Control: public, max-age=60, stale-if-error=86400
How it works:
  1. Normal situation: Updates every 60 seconds
  2. Server error occurs: Uses stale cache for up to 86400 seconds (1 day)
  3. Server recovers: Returns to normal updates
Use case: Maintain service even during server failures

Vary Header

Allows different caches based on request headers for the same URL.
Cache-Control: public, max-age=3600
Vary: Accept-Language
Meaning: Separate cache by Accept-Language header value Example:
GET /api/products (Accept-Language: ko) → Korean response cached
GET /api/products (Accept-Language: en) → English response cached
Commonly used Vary:
  • Accept-Language: Multi-language support
  • Accept-Encoding: Compression method (gzip, br)
  • Authorization: Authentication status

Sonamu vs BentoCache

Sonamu has two caching mechanisms:
Browser/CDN Caching
@api({
  httpMethod: 'GET',
  cacheControl: { maxAge: 3600 }
})
async getProducts() {
  return this.findMany({});
}
Location: Browser, CDN, Proxy Control: HTTP headers Target: Final response received by client Characteristics:
  • Reduces network traffic
  • Client controlled (browser manages cache)
  • Difficult to invalidate

Combined Usage

Using both together provides maximum performance:
@cache({ ttl: "10m", tags: ["product"] })  // Server cache
@api({
  httpMethod: 'GET',
  cacheControl: { maxAge: 60 }  // HTTP cache
})
async getProducts() {
  return this.findMany({});
}
Effect:
  1. First request: DB query (slow) → Server cache + HTTP cache
  2. Within 60 seconds: Returns from browser cache (very fast, no server request)
  3. 60 seconds - 10 minutes: Server request but uses server cache (fast, no DB query)
  4. After 10 minutes: DB query (slow) → Cache again

Caching Layers

Role of each layer:
  1. Browser Cache: Per-user cache, fastest
  2. CDN Cache: Regional cache, reduces network latency
  3. Server Cache: Reduces DB load, instant invalidation possible
  4. Database: Source of truth

Practical Examples

1. Public API (Same for all users)

@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 300,  // 5 minutes
  }
})
async getProducts() {
  return this.findMany({});
}

2. Personalized API (Different per user)

@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'private',
    maxAge: 60,  // 1 minute
  }
})
async getMyOrders(ctx: Context) {
  return this.findMany({
    where: [['user_id', ctx.user.id]]
  });
}

3. SSR Page

registerSSR({
  path: '/products',
  cacheControl: {
    visibility: 'public',
    maxAge: 10,
    staleWhileRevalidate: 30,  // Use stale for 30 seconds after expiration
  }
});

4. Static Files (With Hash)

// Filename: bundle-abc123.js
{
  cacheControl: {
    visibility: 'public',
    maxAge: 31536000,  // 1 year
    immutable: true,  // Never changes
  }
}

5. Sensitive Data

@api({
  httpMethod: 'POST',  // Mutation
  cacheControl: {
    noStore: true,  // No cache storage
  }
})
async createPayment(data: PaymentSave) {
  return this.saveOne(data);
}

Precautions

Precautions when using Cache-Control:
  1. No caching for mutation requests: Use no-store for POST, PUT, DELETE requests
    // ❌ Wrong example
    @api({
      httpMethod: 'POST',
      cacheControl: { maxAge: 60 }  // Caching a mutation
    })
    
    // ✅ Correct example
    @api({
      httpMethod: 'POST',
      cacheControl: { noStore: true }
    })
    
  2. Private or no-store for personal information: Data that should not be exposed to other users
    // ❌ Dangerous: public can be cached on CDN
    @api({
      cacheControl: { visibility: 'public', maxAge: 60 }
    })
    async getMyProfile() { ... }
    
    // ✅ Safe: private or no-store
    @api({
      cacheControl: { visibility: 'private', maxAge: 60 }
    })
    async getMyProfile() { ... }
    
  3. Short TTL for frequently changing data: Prevent stale data
    // ❌ Stock changes frequently but cached for 1 hour
    @api({
      cacheControl: { maxAge: 3600 }
    })
    async getStock() { ... }
    
    // ✅ Use short TTL
    @api({
      cacheControl: { maxAge: 10 }
    })
    async getStock() { ... }
    
  4. Consider s-maxage when using CDN: CDN can cache longer
    @api({
      cacheControl: {
        maxAge: 60,      // Browser: 1 minute
        sMaxAge: 300,    // CDN: 5 minutes
      }
    })
    async getData() { ... }
    

Next Steps