The architectural difference — not just cheaper, structurally different
What the Worker actually does
The Worker is a small program that runs on Cloudflare's network, sitting between every visitor and the storage layer. For every request it makes one decision, in order:
1
Is it already at the edge cache?Fastest path — serve instantly, nothing else runs.
2
Is it in R2, the permanent store?Serve from there — this covers almost every request once a file has been seen once.
3
Not in R2 yet — ask originweb01, just this onceServe the file, and quietly save a permanent copy into R2 in the background so originweb01 is never asked for that file again.
The core shift: Azure has only ever been a temporary cache in front of originweb01 — a single layer with no memory of its own. Cloudflare adds a Worker that actively manages a permanent second copy of every asset. That's the resilience upgrade this migration is really about, cost savings aside.
02
How it works — email open, start to finish
Azure and Cloudflare run side by side. Old campaigns keep working; new campaigns route through the new backbone. Both land on the same storage layer.
Both paths converge here. Old or new link, the request hits the same Worker — check R2 first, fall back to originweb01 if needed, then store it in R2 for next time. One backend, two entry points.
03
Purge process during coexistence
Correcting a new campaign's asset shortly after launch is two steps:
1
Delete the file from R2R2 is the source of truth — this removes the old version for good.
2
Purge the Cloudflare edge cacheForces a fresh fetch, which re-populates R2 automatically.
Azure keeps its own separate cache for old campaign links. No extra steps needed — old campaigns are never edited after sending, so Azure's cache never enters the purge workflow.
04
Purge via Dashboard — exact clicks
The actual navigation path in the Cloudflare dashboard, step by step. Two parts — edge cache first, then R2.
Part A — Edge cache
1
Expand Domain
2
Click Overview
3
Click acmeretail.com
4
Click Caching in the left menu
5
Click Configuration
6
Click Custom Purge
7
Click URL
8
Enter the folder path matching the client folder on originweb01:
Navigate to the path needed and delete the folder or file
Do both parts. Deleting from R2 alone still leaves the old version served from edge cache. Purging the edge cache alone just re-fetches the same stale version from R2. Order doesn't matter between the two, but both steps are required.
05
Migration plan — template URL change
New campaigns adopt the new URL naturally, one at a time. Existing reusable templates are a separate, finite task.
Existing template inventory to update
Unlike a one-time campaign blast, these get sent or triggered again — so their URLs need to be updated directly, not left to age out naturally.
Template type
QuickBite Grill
Riverside Café
Ongoing, Triggers, Adhocs
40
55
95 templates total, not zero. The "no bulk edits" framing above applies to brand-new one-off campaigns only. These 95 — Ongoing, Triggers, and Adhocs across both clients — are reused, so each needs its URL updated directly as part of this migration, tracked as a discrete checklist rather than left to naturally age out.
Planning the change
Purge process after the change
Same two steps as during coexistence — the only difference is Azure is no longer part of the picture for that campaign. Old campaigns keep working through Azure until they stop being opened.
06
Future improvement — automated purge
Next step beyond this migration: connect Cloudflare's API directly to CampaignHub.
Dependency: QuickBite Grill (MailerPro) needs to move to CampaignHub before this applies. Until then, QuickBite Grill stays on the originweb01 → Worker → R2 pattern.
Originweb01 stays online regardless. This diagram describes the publishing workflow only — new campaigns skip the originweb01 staging step and go straight to R2. It does not mean originweb01 gets shut down. Keeping originweb01 alive as a live serving fallback protects against R2 or Worker-specific problems (these do happen — R2 had a real regional incident in Feb 2026) independent of a full Cloudflare outage. Same logic already applied to Azure during its wind-down: keep the safety net running, retire it deliberately later, not as a side effect of a workflow change.
If Cloudflare (Workers and/or R2) has an outage
Originweb01 being kept online only helps if there's an actual way to route to it. Two levels of fallback, depending on severity:
1
Worker misbehaving specifically (bug, bad deploy)Remove the Worker route (see Troubleshooting section) — Cloudflare's edge stays in the path, just skips the Worker, proxying straight to originweb01.
2
Broader Workers/R2 outage — full bypassSwitch assets.acmeretail.com from Proxied (orange cloud) to DNS only (gray cloud) in the Cloudflare dashboard. This routes traffic directly to originweb01's IP, fully bypassing Cloudflare's network for this hostname — the most complete fallback, independent of which specific Cloudflare service is having the problem.
This is the concrete payoff of keeping originweb01 online. A resilience backup only counts if there's a real, fast way to fail over to it — this is that path.
07
Security improvements
What's been hardened, and two real issues caught before they became bigger problems.
Built in from the start
✓
Least-privilege accessEvery deploy token scoped narrowly and time-limited — deleted right after use, never left standing
✓
Path & method restrictionsWorker only ever accepts /Email/* paths and GET/HEAD requests — everything else is rejected outright
✓
Origin response validationRedirects, cookies, and unsafe cache signals from originweb01 are rejected rather than trusted and passed through
✓
Security headers on every responseIncluding a strict content policy on HTML pages specifically, since those render directly in a browser
✓
Reliability hardeningOrigin requests now time out properly on a stalled transfer, not just a slow first response
Caught in Testing Phase — real issues, found and fixed
R2 backfill was silently failing, 100% of the time. Every image still loaded correctly for recipients, but the background save into permanent storage was failing on every single file — meaning the resilience layer wasn't actually building anything. Found by reading logs, reproduced, fixed, and verified against real traffic. No recipient was ever affected.
Customer data was landing in system logs. Personalization data — name, mailing address — rides in the link when someone opens a "View in Browser" email. Cloudflare's automatic logging was capturing that in full. Turned off immediately once found; the Worker's own diagnostic logging was unaffected and already excluded this data by design.
Why this matters: both issues were caught because logging was already in place and someone was actually reading it — not because anything visibly broke. That's the same discipline applied throughout this project: verify with real evidence, don't assume something works just because nothing looks wrong.
08
Next steps
Four actions before this goes live.
1
Schedule a test windowPick a low-traffic time to run the live test, stakeholders aware in advance.
Action: schedule
2
Test that the Worker worksConfirm it serves from R2, falls back to originweb01 when needed, behaves as designed.
Action: verify
3
Migrate files from originweb01 to R2Populate R2 with existing assets ahead of cutover, instead of relying on gradual backfill.
Action: migrate
4
Confirm Azure still works side by sideValidate Azure keeps serving old links correctly through the new backend.
Action: validate
09
Troubleshooting — if the Worker isn't working
A safe, fast fallback exists if something goes wrong. No DNS changes, no waiting.
How to tell
•
Images or templates failing to loadon assets.acmeretail.com specifically
•
Error responses instead of assetsa blank page, a 500, or a plain-text error where an image should be
•
Worker errors visible in CloudflareWorkers & Pages → assets-r2-primary-router → Metrics or Logs showing failed requests
Traffic instantly goes straight to originweb01Through Cloudflare's standard proxy — no Worker, no R2, no custom logic in the way
3
Site keeps workingExactly like Azure does today — just through Cloudflare instead — while the Worker gets fixed
Takes under 60 seconds. No DNS changes, no waiting for propagation. This is a deliberate design choice — removing the route is the single fastest lever available if anything about the Worker needs attention.
What's temporarily paused: new files won't get backfilled into R2 while the route is off, and originweb01 becomes the sole source of truth again — same as before this migration. What's not lost: anything already saved in R2 stays there, untouched, ready the moment the route is restored.
10
Deploy Guide — assets-r2-primary-router
Step-by-step reference for deploying, testing, and maintaining the Worker.
Files required
Place all four in the same folder on your machine.
JS
assets-r2-primary-worker-cache.jsThe Worker code — cache-enabled, current production version
TOML
wrangler.tomlDeploys the cache-enabled version — this is the one you'll normally use
JS
assets-r2-primary-worker-cache-bypass.jsAlternate Worker code, no edge caching
TOML
wrangler-cache-bypass.tomlDeploys the bypass version — only if you deliberately switch
One-time setup (already done — for reference)
R2 bucket acme-email-assets created
SSL/TLS mode: Full (strict)
Always Use HTTPS: on
R2 bucket confirmed no public custom domain
To redeploy the Worker
1
Confirm Node.js and Wrangler are installed, update if neededCheck both in Command Prompt:
node --version
wrangler --version
Node: v16.17.0 or higher — install from nodejs.org if missing. Wrangler: the same command handles both a fresh install and an update — no separate "update" command needed:
npm install -g wrangler
Run this if wrangler --version says "command not found," or the version looks out of date. Safe to run any time — if already current, it just confirms that.
2
Create a scoped API tokendash.cloudflare.com → profile icon → My Profile → API Tokens → Create Token.
Under API token templates, find "Edit Cloudflare Workers" specifically — the list includes several similarly-named options (Workers AI, Read billing info, WordPress, etc.), so look for that exact wording.
Use template → Account Resources → Include → Acme. Zone Resources → Specific zone → acmeretail.com.
Set a TTL — a short window (e.g. today → tomorrow). A backstop in case manual deletion (Step 4) is ever forgotten — complements, doesn't replace, deleting it after use.
Continue to summary → Create Token → copy it now, shown once.
3
Deploy via Command PromptNavigate to your folder, set the token, then deploy:
cd C:\path\to\your\folder
set CLOUDFLARE_API_TOKEN=paste-your-token-here
wrangler deploy --config wrangler.toml
Do not rename this file to wrangler.toml — that reintroduces the exact ambiguity risk this setup avoids. The --config flag exists specifically so you never need to rename anything. Keep the rule simple: wrangler.toml is always the cache-enabled version; switching to bypass is always a deliberate flag typed at deploy time.
4
Delete the tokenMy Profile → API Tokens → find it → delete. Generate a fresh one next time — never reuse.
5
Verify the deploy actually took the configSome Observability settings have previously only persisted correctly via a Wrangler-based deploy, not the dashboard code editor. After every deploy, check:
Observability → Logs → "Include Invocation logs" is OFF (SendGrid personalization data rides in the query string on WebLink.html requests)
Known recurring issue — if either setting has reverted, re-check and re-fix until a Wrangler-based deploy is confirmed to hold both settings through a subsequent redeploy.
Test after deploying
Enabling debug headers — edit assets-r2-primary-worker-cache.js → find exposeDebugHeaders: false near the top → change to true → redeploy → test → set back to false → redeploy again before real campaign traffic runs through it.
Test
URL
Expected x-served-from
R2 hit
Any known existing file under /Email/
r2
Originweb01 fallback
A file not yet in R2
originweb01-backfilling first, r2 on reload
Not found
A path that doesn't exist
not-found (404)
Outside scope
Any path not starting with /Email/
not-found (404)
Wrong method
A POST request
405 Method Not Allowed
Also check Workers Logs — confirm R2 backfill stored: <key> appears, confirming the resilience layer is actually writing to R2, not just serving from originweb01 every time.
Cache purge reference (for dev team)
1
Delete from R2Cloudflare dashboard → R2 → acme-email-assets → browse to the file → delete.
2
Purge the Cloudflare edge cacheacmeretail.com → Caching → Configuration → Purge Cache → Custom Purge → Prefix → enter the affected path:
https://assets.acmeretail.com/Email/quickbite/
Both steps required — purging edge cache alone re-fetches the stale R2 version. (Cache-bypass variant: only Step 1 needed — there's no edge cache to purge.)