Cloudflare CDN propagation
Overview
When bouine sits behind Cloudflare (or any scenario where Cloudflare caches responses delivered through bouine), invalidating a URL in bouine is not enough — the entry may also be cached at the Cloudflare edge.
The Cloudflare CDN propagation feature forwards bouine purge, ban, and refresh operations to the Cloudflare Cache API so both caches are invalidated atomically from the operator’s point of view.
Mapping strategy
| bouine operation | Cloudflare API call |
|---|---|
POST /v1/purge (URL) | PurgeSingleFile (exact URL list) |
POST /v1/ban with surrogate_key | PurgeByTags |
POST /v1/ban with literal path_regex (no metacharacters) | PurgeByPrefixes |
POST /v1/ban with literal host_regex | PurgeByHostnames |
POST /v1/refresh (URL) | PurgeSingleFile |
POST /v1/ban with complex regex | skipped — bouine_cloudflare_purge_skipped_total incremented |
Regex-based bans that contain metacharacters (e.g. .*, [0-9]+, |) cannot
be expressed as Cloudflare prefix or hostname purges and are therefore skipped.
The bouine_cloudflare_purge_skipped_total{reason="..."} counter records each
skipped invalidation so you can alert on them.
Async mode (default)
By default async: true. The admin API returns 200 OK immediately; the
Cloudflare API call runs in a background goroutine. This keeps invalidation
latency visible to operators at bouine’s speed (~1 ms) rather than Cloudflare’s
(~50–300 ms round-trip).
Set async: false only when you need synchronous confirmation that the CF edge
has acknowledged the purge — for example, during a scripted deployment where the
next step must not run until both caches are empty.
Configuration
cloudflare:
# zone_id is the Cloudflare zone identifier, visible in the CF dashboard URL.
# Non-secret; safe to commit.
zone_id: "your-zone-id"
# api_token must have the "Cache Purge" permission for this zone.
# Leave empty and inject via the CF_API_TOKEN environment variable instead
# (see Kubernetes section below).
api_token: "" # prefer env var
# async: true (default) — admin responses return immediately; CF call runs
# in a background goroutine.
# async: false — blocks the admin response until CF confirms the purge.
async: true
# Timeout for individual Cloudflare API calls. 0 = default (10s).
timeout: 10s
# Selects which bouine operations are forwarded to Cloudflare.
propagate:
purge: true # POST /v1/purge
ban: true # POST /v1/ban
refresh: true # POST /v1/refreshDisabling propagation selectively
Set any propagate.* flag to false to suppress forwarding for that operation.
For example, to forward only tag-based bans and not URL purges:
cloudflare:
zone_id: "your-zone-id"
propagate:
purge: false
ban: true
refresh: falseDecoupling bouine and Cloudflare cache lifetimes
A common pattern is for the origin service to emit Cache-Control headers
that are intended for the browser or the Cloudflare edge — not for bouine.
For example, an API emitting Cache-Control: max-age=60 wants the browser
to re-check every minute, but you want bouine to cache the response for an
hour to shield the origin.
Without any override, bouine would cache for 60 s, and Cloudflare would also
see max-age=60 and cache for 60 s. Both are correct, but the origin is
hit far more often than necessary.
ttl_override lets you separate the two lifetimes. bouine stores the
response for however long you specify; the upstream’s headers are forwarded
byte-for-byte unchanged to Cloudflare (and the browser):
routes:
- name: api
match: { path_prefix: /api/ }
pool: backend
cache:
ttl_override: 1h # bouine caches for 1 h
stale_while_revalidate: 5m
stale_if_error: 24hWhat each layer sees:
| Layer | Receives | Caches for |
|---|---|---|
| bouine | Response body + Cache-Control: max-age=60 | 1 h (override) |
| Cloudflare edge | Cache-Control: max-age=60 (forwarded unchanged) | 60 s |
| Browser | Cache-Control: max-age=60 | 60 s |
The origin is hit at most once per hour per bouine node, while Cloudflare and the browser still honour the service’s intended 60-second freshness window.
Combining this with the Cloudflare CDN propagation feature (purge / ban / refresh) ensures that when you do need to invalidate content, both bouine and the Cloudflare edge are cleared together.
See the full reference in Cache policy → TTL override.
Kubernetes deployment
1. Create the token Secret
kubectl create secret generic bouine-cf-token \
--from-literal=CF_API_TOKEN="<your-Cache-Purge-API-token>" \
-n bouine2. Reference the Secret in your values file
The Helm chart has a dedicated cloudflare stanza for the Secret reference.
The zone ID and propagation settings live inside the config block (they are
rendered into the bouine config file, not set as env vars).
# values.yaml
# Helm-level Cloudflare settings — controls secret injection only.
cloudflare:
apiTokenSecretName: bouine-cf-token # name of the Secret
apiTokenSecretKey: CF_API_TOKEN # key inside the Secret (default shown)
# bouine config block — rendered into /etc/bouine/config.yaml.
config:
cloudflare:
zone_id: "your-zone-id"
# api_token is not set here; the chart injects CF_API_TOKEN as an env var.
async: true # default; omit for same behaviour
timeout: 10s
propagate:
purge: true
ban: true
refresh: trueThe Helm chart injects CF_API_TOKEN from the named Secret as an environment
variable. bouine reads it at startup when cloudflare.api_token is empty in
the config file.
Alternative: use extraEnv
If you manage secrets outside the Helm chart (e.g. via External Secrets
Operator), you can inject the token through extraEnv and leave the
cloudflare.apiTokenSecretName field empty:
extraEnv:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: bouine-cf-token
key: token
config:
cloudflare:
zone_id: "your-zone-id"
async: true
propagate:
purge: true
ban: true
refresh: trueMonitoring
| Metric | Labels | Description |
|---|---|---|
bouine_cloudflare_purge_total | operation, status | Total CF API calls by operation (purge, ban, refresh) and outcome (ok, error) |
bouine_cloudflare_purge_duration_seconds | operation | Latency histogram of CF API calls |
bouine_cloudflare_purge_skipped_total | reason | Invalidations not forwarded (CF disabled or incompatible regex) |
Useful PromQL
# Error rate over the last 5 minutes
rate(bouine_cloudflare_purge_total{status="error"}[5m])
# p99 CF API call latency
histogram_quantile(0.99, rate(bouine_cloudflare_purge_duration_seconds_bucket[5m]))
# How many bans were skipped due to regex incompatibility
increase(bouine_cloudflare_purge_skipped_total{reason=~".*metacharacter.*"}[1h])Status endpoint
curl -s http://localhost:9000/v1/cloudflare/status \
-H "Authorization: Bearer <token>" | jq .{
"enabled": true,
"zone_id": "your-zone-id",
"async": true,
"last_error": null,
"last_success_at": "2026-05-27T14:05:00Z"
}Error handling and retries
Cloudflare API errors are retried with exponential back-off:
- 429 (Rate Limit) — retried up to 3 times with jitter, honouring
Retry-Afterif present. - 5xx (Server Error) — retried up to 3 times.
- 4xx (Client Error) — not retried; logged at
warnlevel and counted inbouine_cloudflare_purge_total{status="error"}. - Network errors — retried up to 3 times.
After all retries are exhausted, the error is recorded in last_error (visible
via GET /v1/cloudflare/status) and logged. The bouine purge/ban/refresh
operation itself is not rolled back — the local cache and cluster peers are
still invalidated regardless of the CF outcome.
Security notes
- The API token requires only the “Cache Purge” permission. Do not use a global API key.
- The token is never logged, never included in traces, and never emitted in error messages.
zone_idis non-sensitive and safe to commit in values files or config.- In
async: truemode, purge goroutines usecontext.WithoutCancelso they are not interrupted when the HTTP request that triggered the purge completes.