Cloudflare Workers and WordPress: serving WooCommerce at the edge
Cloudflare Workers runs JavaScript at the edge of Cloudflare’s global network. WordPress runs PHP on a single origin server. Putting Workers in front of WordPress is the architecture that lets a WooCommerce store inherit edge-network performance without rewriting the CMS. The trade-offs are real and worth naming explicitly.
This article anchors to the Headless WordPress service pillar and pairs with the ISR vs SSR rendering decision and the economics of headless WordPress.
TL;DR
- Workers handles the read path, WordPress handles writes and authoring.
- WooCommerce catalogue and product detail render at the edge; cart and checkout stay on the origin.
- Cache invalidation is webhook-driven, not time-based.
- Cloudflare Pages caps the _redirects file at 2000 rules, plan around it.
- Workers free plan caps the compressed bundle at 1 MB; the paid plan raises this.
What Cloudflare Workers actually is
Workers is a V8 isolate runtime running JavaScript, TypeScript, and WebAssembly inside Cloudflare’s edge network (Cloudflare’s network page lists the data centres). It is not Node.js, not a Lambda container, and not a place to run PHP. WordPress stays on its origin; the Worker is a JS reverse proxy that sits in front of it.
A few characteristics that shape what you can and cannot do on Workers in front of WordPress:
- No PHP. A Worker cannot execute
wp-load.php. It can fetch from/wp-json/wp/v2/postsand from/wp-json/wc/v3/products, transform the response, and return HTML or JSON. The WordPress install handles every authenticated mutation. - CPU is the budget that bites first. Free plan allows roughly 10 ms of CPU per request, paid raises that toward 30 ms and Unbound goes higher. A static-rendered product page reading from Workers KV costs almost nothing; an SSR template loop that aggregates four
/wp-json/calls and runs Markdown-to-HTML on the description can blow the limit on the free plan. - Two storage layers, two consistency models. Workers KV is eventually consistent and fast for reads at the edge; writes can take up to a minute to propagate globally and burst writes get throttled. D1 is a strongly consistent SQLite at the edge. For WooCommerce, KV is fine for cached
/productslistings; live stock counters belong on the WordPress origin or in Durable Objects. - Bundle size is real. The compressed Worker script ships at 1 MB on the free plan, 10 MB on paid. A full Astro or Next.js app with images and locale bundles needs the paid tier.
- Adjacent products solve overlapping problems. Cloudflare Pages Functions sits in the same runtime and is the ergonomically correct home for an Astro or Next.js app. Vercel Edge Functions and AWS Lambda@Edge solve similar shapes with different cost models. Workers wins when WordPress already sits behind Cloudflare DNS and the team wants one platform for cache, WAF, and rendering.
Code written against Web Platform APIs (Fetch, Request, Response, Streams, Web Crypto) runs without changes. Code that depends on the Node standard library, native modules, or filesystem access does not.
The two-layer architecture
In a Workers + WordPress build, two systems share responsibility:
WordPress origin. Authoring (Block Editor, REST API, WP-CLI), write operations (post create, update, delete; order creation; user registration), and the source of truth for content and product data.
Cloudflare Workers edge. Read path rendering (HTML generation), API aggregation (combining multiple WordPress REST endpoints into one front-end response), edge caching (per-route, per-tag), and cross-cutting concerns (geo-routing, A/B variants, header rewriting).
The boundary is sharp. Anything that mutates state lives on the WordPress origin. Anything that reads, renders, or aggregates can live on Workers. Cart and checkout sit on the boundary line because they are stateful but read-heavy.
Concrete WooCommerce patterns that move to Workers
Some Workers-in-front-of-WooCommerce patterns we have shipped or seen ship cleanly:
Edge cache for /wp-json/ reads. A Worker route matches /wp-json/wc/v3/products* and serves from cache when the cache key (URL plus a small set of vary headers) is fresh. Stock-sensitive endpoints like /wp-json/wc/v3/products/<id> get a shorter TTL plus webhook invalidation on the woocommerce_product_set_stock action. The PHP origin stops handling product-listing fan-out from product feed exporters, price scrapers, and bot crawl traffic.
Cart-aware page caching. The catalogue page is cached, but the mini-cart fragment is not. The Worker reads the wp_woocommerce_session_* cookie, fetches the cart fragment from origin, and stitches the response together. The catalogue HTML stays cacheable across all anonymous users; the cart fragment is per-session and uncached.
Image transformation at the edge. WooCommerce product images upload to the WordPress media library at full resolution. A Worker route in front of /wp-content/uploads/ resizes, re-encodes to AVIF, and caches at the edge. The origin stops running Imagick on every uncached request, which is one of the loudest CPU sources on a busy WooCommerce box.
Geo-targeted redirects and A/B variants. Workers reads cf-ipcountry and rewrites /shop/ to /shop/de/ for German visitors, or assigns an A/B cohort cookie and serves variant HTML, all without a round trip to PHP. Running the same logic in WordPress means hitting wp-load.php to read is_eu_visitor() from a plugin.
Custom WAF rules beyond WAF Pro defaults. Block POST to /xmlrpc.php, rate-limit /?s= search queries from a single ASN, drop requests that contain wp-config in the path. Workers gives a programmable layer above the managed WAF without paying for Enterprise rule slots.
The shape of what stays on the WordPress origin: Block Editor saves, order creation from authenticated checkout, payment-gateway callbacks, admin sessions, and the webhook fan-out that triggers cache invalidation on the Worker. The PHP server scales to editorial volume plus order volume, not to anonymous catalogue traffic.
Where to draw the boundary
Three categories of route, three different rendering rules:
Static or ISR at the edge. Marketing pages, blog posts, category archives, product detail pages. Cached aggressively with webhook-driven invalidation.
SSR at the edge with no cache. Cart, account, dashboards, checkout pages that are personalised and session-driven. Workers executes the render every request; WordPress provides the data via REST.
Origin-only, no Worker. WP Admin (/wp-admin/), the Block Editor, and WordPress core REST endpoints used internally for authoring. The Worker route configuration explicitly excludes these from the edge layer.
The common mistake is trying to move the cart to the edge cache. The cart is per-user state; caching it cross-user is a security incident waiting to happen. SSR-at-the-edge is the right model: the Worker renders the HTML for that user’s cart, but does not cache the result.
Cache invalidation, the load-bearing piece
ISR at the edge is correct only when invalidation is correct. The pattern that works:
Tag every cached response. Each rendered page is tagged with the WordPress post IDs, term IDs, and product IDs it references. Cloudflare Workers cache supports tag-based purging via the cache API.
Webhook on every WordPress write. Publish, slug change, post deletion, stock update, price change. Each fires a webhook to a Workers route that translates “post 8421 changed” into “purge tag wp-post-8421” via the Cloudflare API.
Time-based revalidation as backstop only. A 1-hour stale-while-revalidate window covers webhook delivery failures. It is not the primary mechanism. A site that revalidates every 60 seconds rebuilds 60 times an hour per page; on 5000 pages that is 300000 unnecessary renders.
Order matters: invalidate then write. When a WordPress publish fires the webhook, the Worker invalidates the tag, then the next request rebuilds the page from the now-current origin. If the order is reversed, a stale render sneaks into cache and persists until the next invalidation.
Limits worth planning around
Cloudflare’s documented platform limits, in one table:
| Limit | Free plan | Paid plan | Where it bites |
|---|---|---|---|
_redirects on Cloudflare Pages | 2000 rules | 2000 rules | Hard ceiling on both. We sit at 1600 in this codebase. |
| Worker bundle size (compressed) | 1 MB | 10 MB Workers paid, up to 25 MB on Workers Unbound | Full Astro / Next.js apps need the paid plan |
| CPU time per request | 10 ms | 30 to 50+ ms on Workers paid and Unbound | SSR with heavy template logic crosses the free-plan ceiling fast; ISR-cached responses pay almost no CPU |
| Subrequests per request | 50 | 1000 on Workers paid | WordPress REST aggregation that fans out to many endpoints runs into the free-plan cap on complex pages |
| Pages output bundle | 100 MB | 25 GB on Pro and Business | Large image sets and prebuilt blog archives can push this on default plan |
The standard architectural answer: design for the paid plan, ship on the paid plan, treat the free plan as a development sandbox.
Astro and Next.js compatibility
Per the our Tech Radar (Q3 2026), both Astro 5+ and Next.js 15 ship official Cloudflare adapters. Astro’s adapter compiles the static + SSR routes to Workers; Next.js’s adapter does the same with App Router. Both produce a deploy artifact that runs on Cloudflare Pages with Workers under the hood.
The selection between them is covered in the Next.js vs Astro decision matrix. For a content-heavy WooCommerce store, Astro is usually the right default because the static-first model maps cleanly to the catalogue. For a more application-heavy build (account, dashboard, complex personalisation), Next.js’s server-component model has the ergonomic advantage.
Either way, the WordPress origin is unchanged. Workers + WordPress is an architecture pattern, not a framework choice.
Failure shapes worth knowing before you ship
A few specific ways we have watched a Workers + WordPress build break or surprise the team:
KV write throttling under bursty invalidation. A bulk product import that touches 5000 SKUs fires 5000 woocommerce_update_product webhooks. The Worker translates each into a KV write on a per-SKU cache key. KV write throughput is rate-limited per namespace, so writes queue and the cache stays stale for minutes. The fix is to debounce on the WordPress side and batch invalidations, or to switch the per-SKU map to a single tag-purge call against the Cloudflare cache API.
Cold-start vs warm-start latency on low-traffic locales. A V8 isolate reused across requests adds almost no startup cost. The first request into a region after a long idle window pays an extra 5 to 50 ms while the isolate boots and the script parses. For a sparsely-trafficked locale page, p99 latency looks worse than p50. Synthetic monitors hitting from many locations smear this out; real user monitoring shows it.
The 1 MB bundle ceiling on the free plan. A full Next.js 15 app with the Cloudflare adapter, react-server-dom, a couple of locale bundles, and the WooCommerce REST client easily clears 1 MB compressed. The deploy fails at upload time, not at runtime. Plan for paid (10 MB) or split rendering across multiple Worker scripts.
Subrequest fan-out on aggregated pages. A category page that fetches the term, the products, the related categories, the breadcrumbs, and the menu hits five /wp-json/ endpoints. Free plan caps subrequests at 50 per Worker invocation, paid at 1000. Aggregation layers that pre-compose the response on the WordPress side, or a single GraphQL endpoint via WPGraphQL, keep the count predictable.
Webhook delivery failures leaving stale cache. WordPress fires the webhook in shutdown action; if the PHP-FPM worker times out or the network drops, the webhook never reaches the Worker. Tag-based stale-while-revalidate of 1 hour catches this; without a backstop, the stale render persists until the next manual purge.
The migration phases (audit, REST + webhook scaffolding, build + cutover, observability) are documented in the howTo schema at the top of this article. We run them inside our headless WordPress service. Pricing is individual because TTFB and TTI depend on the origin’s plugin load, the framework choice, and the live traffic shape; the benchmark scaffold documents the measurement protocol we hold ourselves to.
Where this fits
This pillar article anchors to the Headless WordPress service and the Cloudflare edge deployment service. For framework choice see the Next.js vs Astro decision matrix. For per-route rendering choice see the ISR vs SSR decision. For SEO migration risks see the headless WordPress SEO patterns checklist. For cost framing see the economics of headless WordPress 2026.
