# Web App Caching and Performance Configuration

# Web App Caching and Performance Configuration

This guide explains how to configure caching and performance settings for a self-hosted web app.

It is written for customers, implementation engineers, and CDN/infrastructure teams configuring production environments.

Use this guide to configure:

* Zapp caching settings
* Environment variables
* CDN / CloudFront cache behavior
* Redis-related runtime settings
* Feed timeout behavior

For full self-hosting infrastructure requirements, see [Web Hosting Guidelines](./self-web-hosting/self-web-hosting.md).

---

## Configuration Areas

Caching and performance are configured in three places.

| Area                  | What it controls                                                                     |
| --------------------- | ------------------------------------------------------------------------------------ |
| Zapp                  | Feed rendering behavior, Feed Server Abort Timeout, component-level loading behavior |
| Environment variables | Runtime caching behavior, Redis behavior, user-group cache behavior                  |
| CDN / CloudFront      | Cache keys, TTLs, route bypass rules, forwarded headers/cookies/query strings        |

Use this rule:

> Zapp controls how the app renders.
> Environment variables control runtime behavior.
> CDN controls which responses can be reused.

---

## Recommended Starting Configuration

For most apps, start with this setup.

### Zapp

```text
Enable Feed Server Abort Timeout: true
Timeout: 1500 ms
```

### Environment Variables

```bash
CONFIGS_TTL=300
CONFIGS_SWR=3600
CLIENT_LOADED_FEEDS_ABORT_TIMEOUT_MS=10000
```

Optional, depending on deployment:

```bash
REDIS_TLS=true
REDIS_USE_CLUSTER=true
NO_INDEX=true
```

Use CACHING_GROUP_COOKIE only when the server-rendered page changes for a small, fixed set of user groups, such as anonymous users, logged-in users, premium users, or non-premium users.

```bash
CACHING_GROUP_COOKIE=<access-token-claim-name>
```

### CDN / CloudFront

For cacheable page routes:

```text
Cache query strings: all
Cache cookies: none by default
Cache headers:
  - CloudFront-Viewer-Country only if SSR differs by country
  - CloudFront-Viewer-Time-Zone only if SSR differs by time zone
```

For user-specific, auth, payment, and player routes:

```text
Disable caching
Forward required cookies
Forward Set-Cookie from origin to browser
Respect origin Cache-Control
```

---

# Feature Configuration

## 1. Feed Server Abort Timeout

Feed Server Abort Timeout prevents slow server-side feed requests from blocking page rendering.

If a feed exceeds the configured timeout:

* The feed request is aborted on the server
* The page continues loading
* The feed is loaded from the client after the page loads

This is useful when one slow feed can delay the full SSR response.

### When to Use It

Use Feed Server Abort Timeout when:

* A page contains multiple feeds
* Some feeds are slow or unreliable
* First page load is too slow
* SSR is important, but the page should not wait indefinitely for every feed
* A slow feed can be loaded after the page appears

### Zapp Configuration

In the **Caching** plugin:

```text
Enable Feed Server Abort Timeout: true
Timeout: 1500 ms
```

Recommended values:

| App behavior                                        |        Timeout |
| --------------------------------------------------- | -------------: |
| Default starting point                              |      `1500 ms` |
| Mostly stable VOD/catalog app                       |      `1500 ms` |
| News, sports, or frequently updated app             | `1000–1500 ms` |
| First paint is more important than SSR completeness |      `1000 ms` |
| No known performance issue                          | `2000–3000 ms` |

Avoid:

* `100 ms`, because it may abort too many feeds
* Values below `500 ms`, unless specifically advised
* `5000 ms`, because it usually gives little performance benefit

---

## 2. Client-Loaded Feed Timeout

`CLIENT_LOADED_FEEDS_ABORT_TIMEOUT_MS` controls how long client-loaded feed requests can run before being aborted in the browser.

This applies to feeds that are loaded from the client, including feeds that load from the client after server-side loading was aborted.

### When to Use It

Use this setting when:

* Client-loaded feeds sometimes hang
* A slow feed hurts the browser experience
* You want to show fallback or empty states instead of waiting too long
* The app uses client-side feed loading for dynamic or frequently updated content

### Environment Variable

Recommended starting value:

```bash
CLIENT_LOADED_FEEDS_ABORT_TIMEOUT_MS=10000
```

This allows client-loaded feeds to run for up to 10 seconds.

Use a shorter value, such as `5000`, when:

* Feeds are often slow
* The page has many feeds
* The UX should fail fast

Use a longer value only when:

* Feed providers are slow but usually return valid data
* The feed is important enough to wait for
* Users are expected to have slower network conditions

---

## 3. Configuration Cache TTL and SWR

Configuration data is cached to reduce origin work and improve response time.

Two environment variables control this behavior:

```bash
CONFIGS_TTL=300
CONFIGS_SWR=3600
```

### `CONFIGS_TTL`

`CONFIGS_TTL` controls how long configuration data is considered fresh.

Example:

```bash
CONFIGS_TTL=300
```

This means config data can be treated as fresh for 300 seconds.

Use a shorter TTL when:

* Configuration changes must appear quickly
* The customer frequently updates layout, navigation, or content configuration
* You are testing in staging

Use a longer TTL when:

* Configuration changes are infrequent
* Traffic is high
* Reducing origin/cache refresh pressure is more important than immediate freshness

### `CONFIGS_SWR`

`CONFIGS_SWR` controls how long stale configuration data can be reused while fresh data is revalidated.

Example:

```bash
CONFIGS_SWR=3600
```

This allows stale config to be served while the app refreshes config in the background.

Use a longer SWR window when:

* Availability is more important than immediate freshness
* The app should keep serving pages if config refresh is slow
* The configuration source may occasionally be unavailable

Use a shorter SWR window when:

* Stale configuration is unacceptable
* The customer frequently changes app structure or navigation

Recommended starting point:

```bash
CONFIGS_TTL=300
CONFIGS_SWR=3600
```

---

## 4. CDN Page Caching

The CDN is the main caching layer for HTML documents and static assets.

Most performance gains should come from CDN cache hits.

### Default Rule

For cacheable page routes, use the smallest cache key that still returns correct content.

Include:

```text
URL path
Query string
Country header, only if content differs by country
Time zone header, only if content differs by time zone
caching-group-cookie, only if SSR output differs by entitlement group
```

Do not include:

```text
Full Cookie header
Session cookies
JWT cookies
User ID
Favorites state
Continue Watching state
Analytics cookies
Third-party cookies
```

### CloudFront Default Page Cache Policy

Use this for most cacheable page routes.

```text
Query strings in cache key: all
Cookies in cache key: none
Headers in cache key:
  - none by default
  - CloudFront-Viewer-Country only if SSR differs by country
  - CloudFront-Viewer-Time-Zone only if SSR differs by time zone
Compression: enabled
```

Origin request policy:

```text
Forward query strings: all
Forward cookies: none
Forward headers:
  - CloudFront-Viewer-Country only if the app needs it
  - CloudFront-Viewer-Time-Zone only if the app needs it
```

### When to Use Short CDN TTL

Use a short CDN TTL for pages that change frequently but can tolerate brief staleness.

Examples:

* News pages
* Sports schedules
* Recently updated rails
* Live-event listing pages

CloudFront cache policy:

```text
Minimum TTL: 0
Default TTL: short
Maximum TTL: short
Query strings in cache key: all
Cookies in cache key: none by default
Headers in cache key:
  - CloudFront-Viewer-Country only if needed
  - CloudFront-Viewer-Time-Zone only if needed
```

For highly dynamic content, prefer:

```text
Cache page shell at CDN
Load fast-changing feeds from the client
Keep real-time APIs non-cacheable or short-lived
```

---

## 5. User-Group Cache Variation

Use user-group cache variation only when the SSR document changes by entitlement or login group.

Examples:

* Premium users see different SSR content than non-premium users
* Anonymous users see different SSR page structure than logged-in users
* Server-rendered metadata differs by subscription tier
* Premium shelves are rendered during SSR only for premium users

Do not use this for per-user personalization.

### Environment Variable

Configure:

```bash
CACHING_GROUP_COOKIE=<access-token-claim-name>
```

Example:

```bash
CACHING_GROUP_COOKIE=subscription_tier
```

The access-token claim should produce low-cardinality values.

Good values:

```text
anonymous
registered
premium
non-premium
vip
kids
```

Bad values:

```text
user_id
email
session_id
jwt_id
device_id
```

If `CACHING_GROUP_COOKIE` is not configured:

* The app does not produce `caching-group-cookie`
* The CDN must not depend on it

### CloudFront Configuration

Use this only for cacheable page routes where SSR differs by group.

Cache policy:

```text
Query strings in cache key: all
Cookies in cache key:
  - caching-group-cookie
Headers in cache key:
  - CloudFront-Viewer-Country only if needed
  - CloudFront-Viewer-Time-Zone only if needed
```

Origin request policy:

```text
Forward query strings: all
Forward cookies:
  - caching-group-cookie
Forward headers:
  - CloudFront-Viewer-Country only if used
  - CloudFront-Viewer-Time-Zone only if used
```

Never include:

```text
Full Cookie header
Session cookie
JWT cookie
User ID
```

Rule of thumb:

> Use `caching-group-cookie` for groups, not users.

---

## 6. Country and Time-Zone Variation

Use country or time-zone variation only when the SSR document changes based on country or time zone.

### Country Variation

Use this when:

* Catalog differs by country
* Availability differs by country
* Regional rails differ by country
* Geo-blocking affects SSR output

CloudFront header:

```text
CloudFront-Viewer-Country
```

Vercel header:

```text
x-vercel-ip-country
```

CloudFront cache policy:

```text
Headers in cache key:
  - CloudFront-Viewer-Country
```

Origin request policy:

```text
Forward headers:
  - CloudFront-Viewer-Country
```

Do not vary by country if the page shell is identical and regional data loads from the client.

### Time-Zone Variation

Use this when:

* Broadcast schedules differ by time zone
* Live sports schedules differ by time zone
* Time-windowed availability affects SSR output
* “Available today” content differs by local time

CloudFront header:

```text
CloudFront-Viewer-Time-Zone
```

Vercel header:

```text
x-vercel-ip-timezone
```

CloudFront cache policy:

```text
Headers in cache key:
  - CloudFront-Viewer-Time-Zone
```

Origin request policy:

```text
Forward headers:
  - CloudFront-Viewer-Time-Zone
```

Do not vary by time zone if schedule data loads from the client.

---

## 7. Non-Cacheable Routes

Some routes must never be cached by CDN because they are user-specific, state-changing, or security-sensitive.

### CDN / CloudFront Configuration

For these routes:

```text
Disable caching
Forward required cookies
Forward required query strings
Forward Set-Cookie from origin to browser
Respect origin Cache-Control
Allow required HTTP methods
```

At minimum, support:

```text
GET
POST
OPTIONS
```

### Routes to Bypass

Authentication and session:

```text
/api/is-logged-in
/api/logout
/api/oauth
/login
/login/token
/logout
/oauth
/users/sign_in
/users/login
/users/password/edit
```

Payment and billing:

```text
/payment
/billing
/api/payment-auth
/api/stripe-create-payment-session
/api/stripe-billing-portal
/successful-payment
/paypal-purchase
```

User-specific data:

```text
/api/favorites
/api/favorite-action
/api/continue-watching
/api/client-feed
/api/preference-editor
/api/maybe-redirect
```

Player:

```text
/player
```

Parental controls:

```text
/lock
/parent-lock
```

---

## 8. Personalized Features

Features such as Continue Watching, Favorites, profile state, and personal recommendations should not define the page cache key.

Recommended setup:

```text
Keep page/document routes shared and cacheable
Render personal widgets after page load
Fetch personal data from non-cacheable APIs
Do not vary CDN cache by user ID, session, JWT, favorites, or watch history
```

Use `CACHING_GROUP_COOKIE` only for low-cardinality entitlement groups, not personalization.

---

## 9. Redis Configuration

Redis is used for:

* Caching Zapp configuration JSONs
* Caching content feeds
* Storing user JWTs

Optional Redis variables:

```bash
REDIS_TLS=true
REDIS_USE_CLUSTER=true
```

Use `REDIS_TLS=true` when the Redis provider requires TLS.

Use `REDIS_USE_CLUSTER=true` when Redis Cluster is used.

Redis should be located in the same region as the origin server when possible.

Clearing Redis may log out users because JWTs are stored there.

---

## 10. Staging Configuration

For staging environments, configure:

```bash
NO_INDEX=true
```

Use this to prevent SEO bots from indexing staging deployments.

Staging should mirror production as closely as possible so caching and performance behavior can be tested before production release.

---

# CloudFront Behavior Summary

Configure CloudFront with separate behaviors for different route types.

| Priority | Behavior             | Example path pattern                            | Cache setup              |
| -------: | -------------------- | ----------------------------------------------- | ------------------------ |
|        1 | Auth/session routes  | `/login*`, `/logout*`, `/oauth*`, `/users/*`    | Disabled caching         |
|        2 | Payment routes       | `/payment*`, `/billing*`, payment APIs          | Disabled caching         |
|        3 | User-specific APIs   | `/api/favorites*`, `/api/continue-watching*`    | Disabled caching         |
|        4 | Player routes        | `/player*`                                      | Disabled caching         |
|        5 | Static assets        | `/_next/static/*`, `/assets/*`, `*.js`, `*.css` | Long-lived caching       |
|        6 | Page/document routes | Default behavior                                | Custom page cache policy |

More specific behaviors should have higher priority than the default behavior.

---

## Static Assets CloudFront Policy

Use this for JavaScript, CSS, images, fonts, and other static assets.

```text
Minimum TTL: 0
Default TTL: 86400
Maximum TTL: 31536000
Query strings in cache key: all
Cookies in cache key: none
Headers in cache key: none, except normalized Accept-Encoding when compression is enabled
Compression: enabled
```

Do not forward cookies for static assets.

---

# Testing Checklist

Before production release, verify:

## Zapp

* [ ] Feed Server Abort Timeout is enabled when needed
* [ ] Timeout value matches the app behavior
* [ ] Slow or non-critical feeds are loaded from the client when appropriate
* [ ] Personalized widgets are not unnecessarily part of SSR output

## Environment Variables

* [ ] `CONFIGS_TTL` is configured
* [ ] `CONFIGS_SWR` is configured
* [ ] `CLIENT_LOADED_FEEDS_ABORT_TIMEOUT_MS` is configured
* [ ] `CACHING_GROUP_COOKIE` is configured only when SSR differs by user group
* [ ] `CACHING_GROUP_COOKIE` does not use user ID, email, session ID, or device ID
* [ ] `REDIS_TLS=true` is set if Redis requires TLS
* [ ] `REDIS_USE_CLUSTER=true` is set if Redis Cluster is used
* [ ] `NO_INDEX=true` is set for staging

## CDN / CloudFront

* [ ] Static assets have a dedicated cache behavior
* [ ] Cacheable page routes use a custom page cache policy
* [ ] Auth routes bypass cache
* [ ] Payment routes bypass cache
* [ ] User-specific API routes bypass cache
* [ ] Player routes bypass cache
* [ ] Query strings are included for page/document routes
* [ ] Full `Cookie` header is not included in the cache key
* [ ] `caching-group-cookie` is included only when SSR differs by user group
* [ ] Session and JWT cookies are not included in the cache key
* [ ] Country header is included only when SSR differs by country
* [ ] Time-zone header is included only when SSR differs by time zone
* [ ] `Set-Cookie` from origin reaches the browser on auth/session routes
* [ ] Origin `Cache-Control` headers are respected

---

# Related Documentation

For full self-hosting CDN cache-key guidance, cookie-based variation, non-cacheable route handling, Redis configuration, server requirements, and infrastructure requirements, see [Web Hosting Guidelines](./self-web-hosting/self-web-hosting.md).
