Skip to content

Content Caching

AppMecca can place an edge cache in front of your services to accelerate static content delivery. When enabled, requests for static assets (JavaScript, CSS, images, fonts) are served from edge locations close to your users instead of hitting your container every time. This reduces latency, lowers container load, and compresses responses automatically.

Content caching is completely separate from the application cache (Redis). The application cache is for your code to store data (sessions, task queues, counters). Content caching is for HTTP responses — it sits between your users and your service and transparently caches responses based on path rules you define.

This whole page is for container services only

A type: static service — one with a static_site: block, deployed to object storage behind the edge — gets nothing from cache.behaviors. The block parses, validates, and is then ignored, because a static bundle is not served through your container at all.

This is the trap: an app that declared behaviors here and saw no effect ended up running nginx in a container purely to set cache headers it could have declared directly.

For a static service, set the headers on the objects themselves with static_site.cache_control, which is the equivalent feature.

This is deploy[].cache, not the top-level cache:

The block on this page lives inside a service in your deploy: list. The top-level cache: block is a different feature entirely (managed Redis).

They also default in opposite directions: the top-level Redis block is off until you write enabled: true, while this one is on as soon as the block exists. Getting the nesting wrong is silently ignored — no error, no warning.

When to Use It

Content caching works best for:

  • Static sites — HTML, CSS, images served by nginx
  • Single-page applications (SPAs) — React, Vue, Svelte builds with hashed asset filenames
  • Frontend services in multi-container apps — the frontend container serves static bundles, the backend API stays uncached

Content caching is not useful for:

  • API endpoints — responses change per request (query params, auth, POST bodies)
  • Server-rendered pages — HTML generated per request (SSR apps like SvelteKit in SSR mode)
  • WebSocket connections — real-time data, caching defeats the purpose
  • Background workers — no HTTP traffic to cache

Enabling Content Caching

Add a cache section to a service in your deploy list:

deploy:
  - name: web
    container: myapp
    port: 80
    health_check:
      path: /health
    cache:
      enabled: true
      default_ttl: 300            # Cache everything for 5 minutes by default
      invalidate_on_deploy: true  # Clear the cache on every deploy

That's it. On the next deploy, AppMecca puts an edge cache in front of this service.

Configuration Reference

These fields go directly under a service's cache: block.

Field Required Default Description
enabled No true Whether content caching is active. Defaults to on because you wrote the block — set false to keep the config but disable it.
default_ttl No 0 Default cache duration in seconds for paths not matched by a behavior rule. 0 means nothing is cached — see below.
invalidate_on_deploy No true Automatically clear the entire cache when a new version is deployed.
behaviors No [] Path-specific caching rules (see below). Container services only — a type: static service must use static_site.cache_control instead. CloudFront allows 25 per distribution; for an app serving several services under one URL, each mounted service also uses two, and exceeding the total is refused with the count.

Edge coverage is NOT in this block

How far your CDN reaches is declared once for the whole app, in a top-level cdn: block — not per service. See Edge coverage below.

Edge coverage

cdn.coverage decides which continents' edge locations may serve your app. It does not change how much is cached — it changes how far the response has to travel, and how far the TLS handshake has to travel, which is usually the larger number.

It lives at the root of your config, not inside a service:

cdn:
  coverage: extended

Why it is app-wide

Your app has one URL and one CloudFront distribution. Coverage was once declared per service, and the widest value any service asked for governed the whole app anyway — so a per-service setting was app-wide in fact while looking scoped. It is declared once now.

Each level keeps everything the one below it has and adds to it:

Value Edge locations
standard (default) North America, Europe, Israel, Türkiye
extended …plus Africa, the Middle East, Asia, Japan and India
global …plus South America, Australia and New Zealand

One cdn: block covers every CDN-backed service in the app, container and static alike.

cdn: and cache: are different things

cdn: (root) is reach — which continents serve your app. deploy[].cache: (per service) is policy — what gets cached, for how long, on which paths. And redis: (root) is neither: it is your ElastiCache datastore.

If your users are outside North America and Europe, set extended

The default has no African, Asian or South American edge, so a viewer in those regions is served from the nearest included continent. Measured from South Africa against a real app: the same page took 0.20 s to connect and 0.95 s to first byte on standard (served from London) versus 0.02 s and 0.29 s on extended (served from Cape Town).

The effect is largest for pages that are not cached, because a cache miss pays the round trips to the edge and the trip onward to your container.

What it costs. Requests are billed at the same rate in every class — only data transfer differs, and only for the traffic that actually shifts to a new region (roughly +29% per GB for African/Middle Eastern edges versus European ones). For most apps this is pennies, and AWS's permanent free tier covers the first 1 TB per month.

A change takes effect on your next deploy, in both directions — widening and narrowing both converge.

enabled: true on its own caches nothing

default_ttl defaults to 0, and 0 means do not cache. A block containing only enabled: true therefore sets up the edge cache and then passes every request straight through to your container.

To actually cache, set a non-zero default_ttl, add behaviors rules, or both.

compress is a per-behavior field only

There is no compress option at the cache: level. Writing one there is silently ignored — it does not fail, it just does nothing. Compression is configured per behavior rule, where it defaults to true.

Behavior Rules

Behaviors let you set different cache durations for different URL paths. They are evaluated in order — the first matching path wins.

redis:
  enabled: true
  behaviors:
    - path: "/assets/*"      # Vite/webpack hashed bundles
      ttl: 31536000           # 1 year (immutable filenames)
      compress: true

    - path: "/api/*"          # API routes — never cache
      ttl: 0
      forward_all_headers: true
      allowed_methods:
        - GET
        - HEAD
        - OPTIONS
        - PUT
        - POST
        - PATCH
        - DELETE
  default_ttl: 300            # Everything else: 5 minutes

If your app puts services on paths, a behavior may not claim another service's path

When your app serves several services under one URL (a path: on a service — see Services), the whole app shares one CDN distribution, and the first matching rule wins for the whole app. A behavior rule that reaches into another service's path would therefore apply your caching to their responses — including cached copies of authenticated pages with cookies stripped. So it is refused, with a message naming both services.

Concretely, if a service is mounted at /api:

Behavior path Result
/api/* refused — that path belongs to the service mounted there
/api/v1/* refused — still inside that service's path
/* refused — this would swallow the whole app
/assets/* fine — no service is mounted there
/apiary/* fine — a different path, despite the shared letters

To cache a mounted service's own paths, declare the behavior on that service — each service has its own cache: block, and a rule declared there may narrow caching anywhere inside its own path.

This does not affect an app whose services each have their own hostname; there, every service has its own distribution and nothing can collide.

Field Required Default Description
path Yes — URL path pattern (supports * wildcards). The only required field.
ttl No 86400 Cache duration in seconds. 0 = no caching (pass-through). Note the default is 24 hours, not "no caching".
min_ttl No 0 Lower bound on the cache duration.
max_ttl No 31536000 Upper bound on the cache duration (1 year).
compress No true Compress responses (gzip/brotli) for this path.
forward_query_strings No false Include the query string when matching cache entries and pass it to your service.
forward_cookies No none Cookie forwarding: none, all, or whitelist.
cookie_whitelist No [] Which cookies to forward when forward_cookies: whitelist.
forward_all_headers No false Forward all request headers to your service. Use for uncached paths.
allowed_methods No [GET, HEAD] HTTP methods to forward. Cached paths only need GET/HEAD.

Omitting ttl caches for a day

ttl is optional and defaults to 86400 seconds. A behavior written to disable caching for a path must say ttl: 0 explicitly — leaving ttl out does the opposite of what you want.

Common Patterns

Pattern 1: Static Site (cache everything)

For pure static content served by nginx — HTML, CSS, images — cache the entire service with a long TTL.

deploy:
  - name: web
    container: static-site
    port: 80
    health_check:
      path: /health
    cache:
      enabled: true
      default_ttl: 86400      # 24 hours
      invalidate_on_deploy: true

Sample apps: static-site

Pattern 2: SPA with Hashed Assets

Modern build tools (Vite, webpack) produce filenames like main.a1b2c3d4.js — the hash changes when the content changes. These files are safe to cache forever because the filename itself is the cache key. The HTML entry point (index.html) should have a shorter TTL so users pick up new deployments.

deploy:
  - name: web
    container: react-app
    port: 80
    health_check:
      path: /health
    cache:
      enabled: true
      behaviors:
        - path: "/assets/*"
          ttl: 31536000       # 1 year for hashed JS/CSS
          compress: true
      default_ttl: 300        # 5 minutes for HTML
      invalidate_on_deploy: true

Sample apps: react-frontend, spa-config-demo, photogallery (SolidJS SPA with assets/ long-TTL behavior), eventpass (Vue 3 SPA).

Pattern 3: Fullstack App (frontend only)

In a multi-container app with a frontend (SPA) and a backend (API), add caching only to the frontend service. The backend serves dynamic API responses and should never be cached.

deploy:
  - name: backend
    container: backend
    port: 8080
    health_check:
      path: /api/health
    # No cache — API responses are dynamic

  - name: frontend
    container: frontend
    port: 80
    health_check:
      path: /health
    environment:
      VITE_API_URL: "${service:backend}"
    cache:
      enabled: true
      behaviors:
        - path: "/assets/*"
          ttl: 31536000
          compress: true
      default_ttl: 300
      invalidate_on_deploy: true

database:
  enabled: true

The backend has no cache: section — its responses always go directly from the container to the user. The frontend's static bundles are served from edge locations.

Sample apps: django-react-fullstack, spring-react-fullstack, express-svelte-fullstack, laravel-vue-fullstack, branch-demo-fullstack, shortlink (SvelteKit + React SPAs with edge caching + /_app/* long-TTL), recipe-cookoff, two-newsrooms.

Pattern 4: Mixed Cached and Uncached Paths

For apps that serve both cached static content and uncached dynamic content from the same service, use behavior rules to selectively cache specific paths.

deploy:
  - name: web
    container: myapp
    port: 8080
    health_check:
      path: /health
    cache:
      enabled: true
      behaviors:
        - path: "/cached/*"
          ttl: 60
          compress: true

        - path: "/live/*"
          ttl: 0
          forward_all_headers: true

        - path: "/api/*"
          ttl: 0
          forward_all_headers: true
          allowed_methods:
            - GET
            - HEAD
            - OPTIONS
            - PUT
            - POST
            - PATCH
            - DELETE
      default_ttl: 10
      invalidate_on_deploy: true

Sample apps: cloudfront-demo (dedicated demo of mixed cache behaviors); photogallery — the SolidJS SPA is cached at 5 min default TTL, assets/* at 24 h, and the FastAPI /img/{id}?w=N responses travel through the edge cache with a 1-year Cache-Control: immutable header set by the app itself.

How It Works

When you add a cache: block to a service, AppMecca:

  1. Places an edge cache in front of the service's normal endpoint
  2. Updates DNS so the service URL resolves through the edge cache instead of straight to the platform router
  3. Applies behavior rules to decide, per path, whether to serve from cache or pass through to the container
  4. Compresses responses at the edge (gzip/brotli) for behaviors with compress: true
  5. Clears the cache on every deploy if invalidate_on_deploy: true

Users still access the same URL (https://myapp-web-acme.dev.example.com). The edge cache is transparent — your application doesn't need to know about it.

Cache Invalidation

When invalidate_on_deploy: true (the default), AppMecca clears the entire edge cache after every successful deployment. This means users always get the latest version within seconds of a deploy.

For apps with hashed asset filenames (React, Vue, Svelte builds), invalidation is technically not needed — the new HTML references new asset filenames, which are cache misses by definition. But enabling it is harmless and ensures a clean slate.

Content Caching vs Application Cache

Content Caching Application Cache
What it caches HTTP responses (HTML, JS, CSS, images) Application data (sessions, counters, task queues)
Where it sits Between users and your service (edge locations) Inside your application code
Configuration deploy[].cache (per-service, on by default when present) cache: (top-level, needs enabled: true)
How your app uses it Transparent — no code changes Explicit — you read/write with Redis client libraries
When to use Static content, SPAs, frontend services Sessions, task queues, key-value caching
Documentation This page Cache

You can use both at the same time. A fullstack app might use the application cache for session storage (Redis) and content caching for its frontend SPA (edge cache).

Next Steps

  • Cache — Application cache (Redis) for sessions, task queues, and key-value storage
  • Services — Ports, health checks, commands, and SPA configuration
  • Domain Routing — Custom domains and URL patterns