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:
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:
- Places an edge cache in front of the service's normal endpoint
- Updates DNS so the service URL resolves through the edge cache instead of straight to the platform router
- Applies behavior rules to decide, per path, whether to serve from cache or pass through to the container
- Compresses responses at the edge (gzip/brotli) for behaviors with
compress: true - 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