# CDN (/en/docs/operations/cdn)

Putting a CDN in front of ImgYoung — cache headers, WebP negotiation and Vary, cache keys for on-demand processing, Cloudflare cache purging, and how redirect mode and hotlink protection behave behind a CDN.

## Recommended setup: proxy mode plus a CDN [#recommended-setup-proxy-mode-plus-a-cdn]

Let the CDN pull from ImgYoung (through your [reverse proxy](./reverse-proxy.mdx)) and keep the access mode at `proxy`:

- ImgYoung reads image bytes from the channels and fails over between them on each origin pull, so the CDN sees one stable origin;
- on-demand processing (`?w=`, `?format=`) and WebP negotiation keep working, and the CDN caches the results;
- the public domain is the CDN's, so set `base_url` in the admin console (**Settings → Runtime settings → Access & links → Public site address**), and add the CDN's origin-pull ranges to `IMGYOUNG_TRUSTED_PROXIES` so login rate limiting counts real visitors; see [Reverse proxy & HTTPS](./reverse-proxy.mdx).

Cache only the image routes `/i/*` and `/o/*`; mark `/api/*` and `/s3/*` as not cacheable. The admin console's `index.html` is served with `no-cache`, and files under `/assets/` have content hashes in their names and a one-year cache lifetime, so the CDN's default rules are fine for them.

## Cache headers [#cache-headers]

Responses from the image routes:

| Response | `Cache-Control` | `ETag` | `Vary` |
|---|---|---|---|
| Original under `/i/<short ID>` | `public, max-age=31536000, immutable` | `"<sha256>"` | `Accept` when a JPEG / PNG / WebP is requested without `format` (or with `format=auto`) |
| Variant under `/i/<short ID>` (resized or converted) | same | `"<sha256>-<width>-<format>"`, width `0` when not resized | `Accept` when the target format was negotiated |
| Original or variant under a `/o/` alias | `public, no-cache` | same as above | same as above |
| Original served as a fallback after processing failed | `no-store` | none | none |
| `If-None-Match` matches | 304 with the matching `Cache-Control`, `ETag` and `Vary` | | |
| 302 (`redirect` mode) | `no-store` | none | none |
| Invalid parameters (400), hotlink rejection (403), not found or expired (404), metadata lookup failure (500, retryable) | none | none | none |

Originals also carry `Last-Modified` (the upload time). The content behind `/i/<short ID>` never changes, which is why it gets a one-year immutable lifetime.

`/o/<user ID>/<object key>` is an alias chosen by an S3 client, and uploading again with the same object key points it at the new image. Its responses may therefore be cached but must be revalidated with the ETag before each use: an unchanged image gets 304, a re-pointed alias gets the new image, a deleted one gets 404. Behind a CDN, `/i/` links cache better.

## WebP negotiation and Vary [#webp-negotiation-and-vary]

When a JPEG / PNG image is requested without a `format` parameter (or with `format=auto`), and the `Accept` header contains `image/webp` and `webp` is among the target formats, the response is a WebP variant; otherwise it is the original format. In other words, **the same URL returns different formats depending on `Accept`**, and both responses (304s included) carry `Vary: Accept`.

The CDN must cache separately per `Accept` (by honoring `Vary: Accept` or adding `Accept` to the cache key); otherwise a cached WebP may be sent to clients that cannot display it. Cloudflare lets you configure `Vary` handling in Cache Rules; for other CDNs, check their documentation. If your CDN cannot do this, remove `webp` under **On-demand processing → Target formats** in the admin console: negotiation stops, at the cost of `?format=webp` no longer being accepted.

## Processing parameters and cache keys [#processing-parameters-and-cache-keys]

- `w` must be one of the width tiers and `format` one of the target formats or `auto`; anything else gets 400. The allowlist bounds how many variants an image can have, so arbitrary parameters cannot create unbounded cache entries.
- ImgYoung ignores any other query parameter, but the CDN's default cache key includes it: appending a random parameter bypasses the cache and hits the origin. Restrict the cache key's query string to `w` and `format`.
- Always write the parameters in the order `?w=…&format=…`. Cache purging only clears URLs in that order (see below); a different order is a different cache entry as far as the CDN is concerned.

## Cloudflare cache purging [#cloudflare-cache-purging]

After an image is deleted, CDN edges keep serving its `/i/` links under the one-year cache lifetime. With Cloudflare cache purging configured, ImgYoung clears the edge cache when an image is deleted.

Configure three settings under **Settings → Runtime settings**:

| Setting | Where | Notes |
|---|---|---|
| Cloudflare Zone ID | **CDN cache purge** group | ID of the zone your site is in |
| Cloudflare API token | **CDN cache purge** group | Needs the Cache Purge permission for that zone; stored encrypted with the master key and never shown again |
| Public site address (`base_url`) | **Access & links** group | Used to build the public URLs to purge; must match the domain on the CDN |

With all three set, the group shows **Purge enabled**; if any is missing, nothing is purged. The process also logs the purge state when it starts. Changes apply on save without a restart.

**When it fires**: the moment an image reference is deleted, whatever the source — deletion in the admin console (single or batch), `DeleteObject` on the S3 endpoint, or expiry of its lifetime. Purging does not wait for the delete grace period or for the channel objects to be removed. Images re-pointed by a deduplication merge are not purged.

**What gets purged**: `<base_url>/i/<short ID>`, plus for every variant that was actually generated for the image either `?w=<width>` and `?w=<width>&format=<format>` (resized variants) or `?format=<format>` (format-only variants).

**How**: as an asynchronous background job calling Cloudflare's purge-by-URL endpoint `POST /zones/<Zone ID>/purge_cache`, at most 100 URLs per call. Failures are retried with backoff by the job queue and never block the deletion itself.

**Not covered**: `format=auto` forms and URLs with other query parameters or a different parameter order; these only go away when the CDN's own cache expires. `/o/` alias links need no purging, since they are revalidated with the origin every time.

## Redirect mode and CDNs [#redirect-mode-and-cdns]

In `redirect` access mode, a request for an original gets a 302 to a channel that allows direct links, is not encrypted and has a public address configured. The bytes then come from the channel (or its own domain or CDN) and do not pass through the CDN in front of ImgYoung. Without such a channel ImgYoung falls back to proxying.

- Every 302 carries `Cache-Control: no-store`, so neither the CDN nor browsers cache the redirect itself: every request comes back to ImgYoung, and channel switches, deletions and expiry take effect immediately.
- Requests that need processing (`w` / `format` parameters, or WebP negotiation) are still processed and served by ImgYoung, not redirected. To make browsers' original-image requests redirect as well, remove `webp` from the target formats.

If you already have a CDN in front of ImgYoung, use `proxy` mode: the CDN already carries the traffic. `redirect` is meant for servers without a CDN and with little upstream bandwidth, or for channels that come with their own public CDN domain (such as a custom domain bound to an object storage bucket).

## Hotlink protection behind a CDN [#hotlink-protection-behind-a-cdn]

ImgYoung's Referer allowlist only applies to requests that reach the origin:

- requests served from the CDN cache never reach ImgYoung and are not checked; the default cache key does not include the Referer, so once an allowlisted site has caused an image to be cached, any site can load it from the CDN;
- ImgYoung's 403 carries no `Cache-Control`; if the CDN caches that 403, allowlisted visitors are rejected too.

Behind a CDN, enforce hotlink protection on the CDN (a rule that blocks by Referer) and leave ImgYoung's allowlist empty or use it only as an extra layer. In `redirect` mode the channel's direct links are exposed, so hotlink protection does not hold there either.
