Update & propagation

How your changes go live

Three assets, three paths. All edited remotely — the publisher's <script src> never changes.

wrapper edge + browser 300s · purge ~250ms cfg edge 86400s · browser 60s · purge ~250ms engine no code TTL · no purge · immutable

The three assets

Asset Wrapper remote Rotation cfg remote Engine not remote
Served at the loader at analyytics.com/w/<id>.js ad list + targeting at cfg.analyytics.com/cfg/<id>.js the popunder engine at cloudfroze.com/e/<id>.js
Lives in KV (w:<id>) R2 (cfg/<id>.js) R2, a per-wrapper copy made at publish
Holds baked POP_CONFIG: frequency, triggers, ad URL (single mode), behavior ad URLs, weights, device + country targeting (general + per-ad) the popunder logic (its behavior is driven by the wrapper's POP_CONFIG)
Update it edit in the builder → Publish (PUT /admin/wrapper, same id) Ad-rotation editor → Save (PUT /admin/adconfig) no admin route — needs delete R2 base + redeploy + re-publish
Cost of edit re-bakes the whole wrapper bytes tiny — no wrapper re-bake heavy / manual (see caveats)

The moment you Save / Publish

Write. Wrapper → KV w:<id>, cfg → R2 cfg/<id>.js.
Purge. The Worker flushes that URL from the edge — ~250 ms with a valid CF_API_TOKEN.
Refetch. The next visitor pulls fresh bytes from origin; the edge re-caches them.
Browsers wait. An already-cached visitor keeps the old copy until its browser TTL — a purge flushes the edge, not browsers.
The embed <script src=".../w/<id>.js"> is never touched. Same id = same URL = no re-embed, ever.

Master propagation matrix

18 layers × 3 assets. Wrapper = KV + purge, 300s edge and browser. CFG = R2 + purge, 86400s edge / 60s browser — and not consumed by the wrapper yet. Engine = R2-direct, never purged, no code TTL, immutable after publish.

Read a row across: the same layer, for each asset. Numbers are exact from the code — WRAPPER_TTL=300, NEGATIVE_TTL=60, cfg Cache Rule edge 86400 / browser 60.

Layer🧩 Wrapper  /w/🔀 CFG  /cfg/⚡ Engine  /e/
Store & origin write KV w:<id> · last-write-wins · read-your-write in origin colo (0s) R2 cfg/<id>.js · strong-ish · next origin read = new bytes R2 e/<id>.js copy made at publish · base variants write-once (deploy never overwrites)
Cross-colo lag up to ~60s KV replicates async · a far colo can re-cache stale KV and pin it +300s none R2 strong-ish at origin none R2 strong-ish at origin
Edge cache TTL (if not purged) 300s (WRAPPER_TTL) 86400s / 1 day via cfg/* Cache Rule · without the rule → R2/CDN default no code TTL Cloudflare R2 custom-domain default — never overridden here
Edge purge on update ~250ms purge() files:[/w/<id>.js] — needs CF_API_TOKEN+CF_ZONE_ID ~250ms purgeCfg() — only when r.ok && success===true none nothing purges cloudfroze.com/e/<id>.js — ever
If purge missing / refused edge holds up to 300s edge holds up to 86400s (1 day), then re-pulls last R2 write always waits the R2 default (no purge exists to reconcile)
Browser cache (max-age) 300s public, max-age=300 60s Cache Rule browser_ttl=60 none set no Cache-Control in code → R2 default
?v= cache-bust no hot path strips ? before cache.match → same edge entry; busts the browser only n/a the src is fixed by the baked prelude n/a URL is baked into wrapper bytes — can't add ?v without a re-publish
Negative cache (unknown/deleted id) 404 max-age=60s (NEGATIVE_TTL) · first Publish's purge clears it ~250ms N/A NEGATIVE_TTL is /w/-only — R2-direct 404 rides the edge default N/A NEGATIVE_TTL n/a — R2-direct 404 until the default TTL
Publisher-domain CDN bypassed cross-origin content isn't proxied bypassed cross-origin content isn't proxied bypassed unless a WP plugin localizes it (see below)
WP full-page HTML cache yes caches page + tag, not content yes prelude runs each view, fetches cfg fresh yes engine URL lives inside wrapper bytes → still fetched client-side
WP defer / async / delay-JS yes only changes when it loads (pop fires later) yes only when cfg is fetched; moot in baked mode yes load-timing only
WP combine / minify / inline no baked into a bundle → frozen until WP cache cleared yes runtime-injected src — static passes can't fold it no if inlined into a bundle → frozen until WP cache regen
WP self-host / load-locally no stale local copy — remote updates never arrive usually runtime src normally not localized; if it is → freezes at copy time no double-freeze: captures bytes and bypasses cloudfroze.com (worst case)
Two updates in a row #2 in ~250ms (each PUT purges) · no purge → #1 pins up to 300s #2 in ~250ms (both purge) · failed 2nd purge → up to 86400s · R2 ends on last write R2 last-write-wins, but no purge → edge may keep the earlier copy until the default TTL
Delete / cascade (DELETE /admin/wrapper) KV del + purge() → 404 neg 60s · no purge → old 200 serves up to 300s (can still fire pops) R2 del + purgeCfg() → ~250ms · else edge up to 86400s (NEGATIVE_TTL n/a) R2 del (cascade) but no engine purge → stale from edge/browser until default TTL · base variants never touched
Client geo cache (targeting input) 12h localStorage __cc_<id> = 43200000ms · caches the visitor country, not the cfg body · empty cc fails open (ad shown)
Runtime mode — (it is the wrapper) baked-snapshot: if(BAKED)apply(BAKED) → never fetches cfg → needs a wrapper re-publish (300s+purge). external-fetch (BAKED null): the only mode where remote cfg edits propagate. bytes immutable after publish · only swapping among the 4 fixed variants changes bytes
Actually live end-to-end? yes freq / triggers / single-ad URL are baked bytes not yet the wrapper doesn't consume window.__ADCFG yet (TODO config.html 786 & 1487) — cfg reaches R2/edge/browser but changes nothing on screen content immutable — a variant swap is the only "change"
Legend. ok = the update still reaches visitors · no = updates are blocked / stale here. Numbers in bold are the exact TTL/latency from the source.

Two saves in a row — the second wins

The second wins. Every Save purges, so a newer save always wins; the long Edge TTL never locks an old version.
t=0    save #1  → purge → live ~250ms → edge caches #1
t=10s  save #2  → purge (drops #1) → live ~250ms → edge caches #2
                                     ↳ #2 wins · #1 was live only ~10s
Two exceptions. A browser that already fetched #1 keeps it for its Browser TTL — 60s cfg / 300s wrapper. A failed purge holds the old copy at the edge up to the Edge TTL — 1 day for cfg.
Browser exception. That visitor jumps to #2 when their own max-age expires. New visitors get #2 immediately — a purge flushes the edge, not browsers.
Purge failure = no CF_API_TOKEN, wrong CF_ZONE_ID, or no Cache Rule. It is the only case where "the first stays for a day" happens.

Verify it propagated — read the served bytes

Read the served bytes. cf-cache-status: HIT = old edge copy · grep the baked url":" → NEW = live, OLD = still cached / KV.
curl -sI https://analyytics.com/w/<id>.js | grep -i cf-cache-status   # HIT = old edge copy
curl -s  https://analyytics.com/w/<id>.js | grep -o 'url":"[^"]*'      # NEW = live · OLD = still cached / KV

HIT with old bytes → wait the TTL or re-purge. New bytes but you still see old → it's your browser cache (hard-refresh / incognito).

Force an update NOW

Purges never reach browsers — already-cached visitors always wait out their own max-age.

Wrapper — fastest ~250ms · 5 steps · re-purge after ~60s for cross-colo lag
StepActionLatency
1Ensure CF_API_TOKEN + CF_ZONE_ID are set, then Publish (PUT /admin/wrapper) — it auto-purges.~250ms
2Or manual: POST /zones/<CF_ZONE_ID>/purge_cache with files:[https://<domain>/w/<id>.js] on every serving domain (purge() only hits the request origin).~250ms / domain
3Re-purge ~60s later to evict any colo that re-cached stale KV during cross-colo lag.+~60s
4If combine / self-host is in play on the publisher, clear that WP plugin cache.until cleared
5Browsers can't be forced — already-cached visitors wait out max-age=300; only a hard-refresh / incognito bypasses it.up to 300s
CFG — ~250ms edge / 60s browser · 6 steps · but re-publish the wrapper if you need the ad to change today
StepActionLatency
1Save (PUT /admin/adconfig) → purgeCfg() fires; PUT returns {purged:true} only when success===true.~250ms
2If {purged:false}: confirm CF_API_TOKEN/CF_ZONE_ID and that the URL is inside the analyytics.com zone — else the edge holds old bytes.up to 86400s
3Verify: GET /admin/adconfig?id= reads R2 directly (bypasses the edge) → shows the true last write immediately.immediate
4Returning browsers re-serve their copy for the Browser TTL.60s
5Country targeting can lag: __cc_<id> caches it 12h (43200000ms) — clear the visitor's localStorage to force fresh geo.up to 12h
6Reality check: baked-snapshot wrappers never fetch cfg, and the wrapper doesn't consume __ADCFG yet — to actually change the ad now, re-publish the wrapper instead.wrapper path (300s+purge)
Engine — no route, no purge · 6 manual steps · fastest real option is swapping to another of the 4 variants at publish
StepActionLatency
1No admin route edits engine bytes — the change is out-of-band.
2Replace the base object in R2 (deploy is write-once and won't overwrite): delete it, then wrangler r2 object put --remote <variant> (or the dashboard).manual
3Re-publish every wrapper using that variant (PUT /admin/wrapper) — that's the only path that copies new bytes to e/<id>.js.R2 write immediate
4No engine purge exists → edge/browser keep the old copy until the R2 custom-domain default TTL (no code number). Purge it manually in the Cloudflare dashboard if you must.until default TTL
5Faster alternative: at publish, pick a different whitelisted variant (one of the 4) — its bytes are copied to e/<id>.js immediately at origin.R2 write immediate
6If the publisher self-hosts / inlines the engine, clear that WP plugin cache too.until cleared
Plan engine changes as a redeploy, not a live push. With no purge and no code TTL, new bytes appear only once the R2 custom-domain default edge TTL lapses (or you dashboard-purge).

Behind a cache plugin / CDN

One exclude list covers all three. Add analyytics.com/w/, cfg.analyytics.com/cfg/ and cloudfroze.com/e/ to every "combine / minify / inline JS", "load third-party JS locally / self-host" and "delay JS execution" exclusion — then verify on the publisher page's served bytes, not only on the origin domains.
Wrapper — safe unless combined or self-hosted. Kept external: new visitors ~250ms, cached browsers up to 300s.

Combine/minify/inline bakes it into a WP bundle (frozen until that cache clears); self-host / "load JS locally" copies it onto the WP server, so remote publishes never arrive. Full-page cache, CDN and defer/async/delay only touch the tag or its timing.

CFG — only self-host can freeze it, because the src is injected at runtime. Moot today: the wrapper doesn't consume __ADCFG yet.

Static combine/minify/inline passes can't fold a document.createElement('script') URL, and full-page cache is fine (the prelude re-requests cfg each view). A self-host / localize plugin that snapshots the runtime URL freezes cfg at copy time until re-localized. Upstream reality dwarfs the plugin layer: baked-snapshot wrappers never fetch cfg at all.

Engine — the hardest stale of the three. Already immutable and never purged, so a WP copy on top is unreachable by any re-publish.

The URL is baked into the wrapper bytes and fetched straight from cloudfroze.com, so the publisher CDN never sees it. Combine/inline bakes it into a WP bundle; self-host double-freezes it — captures the bytes and bypasses cloudfroze.com, so even a variant-swap re-publish never reaches those visitors.

Gotchas

Three. Cfg edits change nothing on screen yet · frequency / triggers / single-ad URL need a re-publish, not a cfg edit · the engine can't be pushed remotely at all.
Rotation cfg is not live yet. The wrapper does not consume window.__ADCFG (wrapper JS pending) — a cfg edit reaches R2 and the edge but changes nothing visitors see.
Wrapper settings = re-publish. Frequency, triggers and the single-mode ad URL are baked into the wrapper bytes, so changing them is a Publish (re-bake). Both are remote; the cfg edit is just lighter.
Engine can't be pushed remotely. Each wrapper serves its own e/<id>.js copy — deliberate: deleting a wrapper 404s its engine, so a direct flood on one id costs $0. Patching means updating the R2 base + re-publishing the wrappers.

Domains: wrapper analyytics.com · engine cloudfroze.com · rotation cfg cfg.analyytics.com · panel panel.analyytics.com.