Skip to main content

How WordPress runs on Cloudflare Workers (PHP in WebAssembly)

Updated 8 min readMarkdown

WordPress core is not modified. PHP itself is compiled to WebAssembly, and a small runtime layer gives it what a server used to: a database connection, a file system, a clock, storage for media and a schedule for cron.

A request, step by step

Visitor ──► Cloudflare edge ──► static files (theme and plugin assets, wp-includes)
│
├──────► page cache (Workers KV), for visitors who are not logged in
│
└──────► Worker next to your database
HostlessWP engine (PHP 8.3) + OPcache + precompiled scripts
WordPress core, your theme and plugins in memory
│
├──► Hyperdrive ──► your MySQL (SSL or Tunnel)
└──► your S3 bucket (wp-content/uploads)
  1. Static files never reach PHP

    CSS, JavaScript, fonts and images from WordPress, your theme and plugins are uploaded as Workers static assets and served from the Cloudflare location nearest the visitor.
  2. Cached pages answer in milliseconds

    A visitor who is not logged in and has no cart gets the page from the page cache, without starting PHP.
  3. Everything else runs WordPress

    The Worker hands the request to PHP, WordPress runs as it would on any server, and its queries go to your MySQL through the database bridge.
  4. After the response, uploads are saved

    Files WordPress wrote to wp-content/uploads during the request are copied to your bucket.

PHP in WebAssembly

HostlessWP runs on the HostlessWP engine: PHP 8.3 compiled to WebAssembly with a server API and runtime written for Workers. Inside a Worker it boots once per instance and then serves requests one after another. Outgoing connections from PHP (SMTP for email, HTTP APIs) use the Workers socket and fetch APIs directly, with no proxy.

  • Code is packed into zip bundles. WordPress, your theme, plugins and translations are bundled at deploy time. They are mounted into PHP's in-memory file system lazily: a file is only unpacked when PHP first reads it. A request reads around 2,000 of a site's 16,000 files, and every unpacked file costs memory.
  • OPcache works. Compiled scripts stay in shared memory for the life of the instance.
  • Function bodies load on first call. A WordPress request loads every function of every file it includes but runs only about a fifth of them. The engine keeps the compiled body of each larger function compressed on the side and loads it the first time the function is called, which saved 15 to 20 MB of memory per request on our WooCommerce demo.
  • Scripts are precompiled. During deploy, hostlesswp renders a few pages locally against your database and packs the OPcache files, so a new instance does not compile 1,700 scripts before its first page.

The database bridge

A Worker reaches a database best through a connection pool. A db.php drop-in replaces WordPress's database class and sends every query to the Worker, which runs it through Cloudflare Hyperdrive. A userland mysqli covers plugins that skip $wpdb.

Hyperdrive pools connections, which means two statements in a row can run on different server connections. WordPress and its plugins assume one connection, so the bridge carries session state with each statement:

  • Session settings are attached to every query as SET_VAR optimizer hints.
  • SELECT @@variable is answered from SHOW VARIABLES.
  • SQL_CALC_FOUND_ROWS paging runs as a parallel COUNT, which brought the REST posts list from 2.5 s to 1.6 s in our tests.

Hyperdrive's query cache is turned off, because WordPress reads its own writes right away. A database that is not reachable from the internet is connected through a Cloudflare Tunnel run from your server.

Placement: run next to the database

A WordPress page runs dozens of queries, one after another. Each one waits for a full round trip to the database, so distance multiplies.

7.7 s

Worker near the visitor

Core home page, Worker in Singapore, database in Virginia: about 230 ms per query, 29 queries.

1.2 s

Worker next to the database

Same page, Worker placed in US East. What hostlesswp sets up, by locating your database and setting placement.region.

0.07 s

Direct connection

Same placement without Hyperdrive in between: about 2 ms per query. Our next step; it needs a TLS fix first.

PHP processing time per warm request, including database waits. Method: benchmarks.

Page cache

Starting PHP on a fresh instance takes seconds, so pages for visitors who are not logged in are kept in Workers KV and answered without starting PHP.

  • Never cached: logged-in users, commenters, password-protected posts, shoppers with a cart, searches, previews, the admin, the login page, the REST API, and any response that sets a cookie or that a plugin marks as not cacheable (WooCommerce cart, checkout and account pages).
  • Kept fresh: publishing, editing, approving comments, changing menus, widgets, the theme, plugins, settings or stock marks every cached page as outdated, as does each deploy. The next visitor still gets the cached copy while the page renders again in the background. No page is served older than 8 hours.
  • Visible: every response carries x-hostlesswp-cache (hit, stale, miss, bypass, skip). Turn the cache off with --no-page-cache.

Media in your bucket

A Worker has no lasting disk, so wp-content/uploads lives in an S3-compatible bucket: Cloudflare R2, AWS S3, Backblaze B2, MinIO or any other service with the S3 API.

  • Every deploy copies new files from your site's uploads folder to the bucket. Nothing is ever deleted there.
  • PHP writes uploads to memory as usual; after the request the Worker copies them to the bucket. Unique file names are checked against the bucket, originals are fetched when you edit an image, and deleting an attachment deletes its files.
  • Media URLs stay /wp-content/uploads/... and are served from the bucket, cached at the edge, without starting PHP. The bucket can stay private; give it a public or CDN address and new media links point there directly.

Sessions, cron and the clock

Logins stay valid

Your site's own keys and salts are deployed as a Worker secret, so cookies issued by your current host keep working on Workers.

WP-Cron on a schedule

Scheduled events run from a Cloudflare Cron Trigger every 15 minutes instead of during page views, which keeps visitors from waiting on them.

A clock that moves

Workers freeze the clock during CPU work. PHP functions that wait for it to move, like uniqid(), would spin forever, so the runtime makes the clock strictly increasing.

Outgoing HTTP

wp_remote_get(), curl_*() and file_get_contents() of a URL go through the Worker's fetch(), so update checks, payment gateways and SDKs work. DNS lookups use DNS over HTTPS.

Sockets

fsockopen() and stream_socket_client() open real TCP connections with Workers' connect(). PHP's OpenSSL does TLS over them, checked against WordPress's CA bundle.

Email

wp_mail() goes out over SMTP from the Worker: through your SMTP plugin's server, or the one you enter during setup, whose login is checked before deploying.

PHP sessions

$_SESSION is stored in your database, in one table created on first use, so a session survives from one Worker instance to the next.

Read-only code

Installing plugins and themes from the admin is turned off, because files written there would vanish with the instance. Change code on the source site and redeploy.

Your settings, once

Answers from setup are saved in hostlesswp-config.php next to wp-config.php (mode 0600, never uploaded), so npx hostlesswp deploy asks nothing again.

Limits that shape it

Limit Value What it means for WordPress
Memory per instance 128 MB PHP, the code bundles and the JavaScript runtime share it. WooCommerce with the common plugins fits; very heavy plugin stacks can go over.
Static asset size 25 MiB per file Larger theme or plugin files are left out of the static assets. Media files go to the bucket instead (up to 2 GB).
Upload size 32 MB by default Configurable with s3.maxUploadMb; the Worker request size limit still applies.
Free plan daily limits 100,000 requests Also 100,000 Hyperdrive queries and 1,000 KV writes a day. Workers Paid ($5/month) raises them.

Read the code

The runtime is a few files: the Worker, the lazy zip file system, the media bridge and the PHP drop-ins.

Open runtime/Get started

Originally published at hostlesswp.com/how-it-works.

About the author

  • Open source CLI that moves a running WordPress site to Cloudflare Workers with one command, keeping its plugins, theme and MySQL database.

No comments yet. Be the first to comment.