<Ghayas/>

Running headless WordPress without a database server

/4 min read/Ghayas Ud Din
Close-up of a circuit board with illuminated traces

The bill that finally annoyed me was $14 a month for a managed MySQL instance serving a blog that published maybe six posts a year. The database was idle roughly 99.7% of the time. I was renting a machine to sit still.

So the CMS behind this site now runs with no database server at all. WordPress admin, the REST API, media uploads, all of it — on infrastructure that costs nothing when nobody is looking at it.

The three things that make WordPress hard to run serverless

People say “WordPress needs PHP” as if that were the problem. It isn’t. PHP runs fine in a function. The actual blockers are:

  • MySQL. A connection-oriented database that expects a long-lived process.
  • A writable filesystem. wp-content/uploads assumes local disk. Serverless filesystems are read-only apart from /tmp, which evaporates.
  • Cron. wp-cron.php piggybacks on visitor traffic, and there is no guaranteed traffic.

Each one has a fix. None of the fixes are exotic any more.

SQLite, kept in object storage

WordPress has had an official SQLite integration since the performance team shipped it as a feature plugin. It is a drop-in replacement for the wpdb layer. The catch in a serverless context is obvious: SQLite is a file, and there is no persistent disk to put it on.

The workaround is to keep the database file in object storage, pull it into /tmp at the start of an invocation, and write it back when something changes. Reads are cheap. Writes are the interesting part.

This is the trade you are making: you give up write concurrency to delete an entire class of infrastructure. If two editors hit publish at the same instant, one of them loses.

For a blog with one author that is a non-issue. For a store taking orders it would be malpractice. I have used this pattern on exactly the sites where the read/write ratio is lopsided, and nowhere else.

Media has to go somewhere real

Uploads were the part I got wrong first. I assumed I could point WordPress at the same object store as the database and be done. You can, technically. You should not: the database store wants to be private, and media wants to be publicly cacheable at the edge.

I use a separate Cloudflare R2 bucket. R2 has no egress fees, which matters more than the storage price once images start getting requested. The free tier covers 10GB, and a blog’s worth of optimized images does not come close.

WP Offload Media handles the rewrite. R2 is S3-compatible but has no first-class provider in the plugin, so it rides the AWS provider with the endpoint overridden:

add_filter( 'as3cf_aws_init_client_args', function ( array $args ) {
    $args['endpoint']                = 'https://' . ACCOUNT_ID . '.r2.cloudflarestorage.com';
    $args['region']                  = 'auto';
    $args['use_path_style_endpoint'] = true;

    return $args;
} );

Two details cost me an afternoon. R2’s pseudo-region is always auto, and the plugin’s bucket-region lookup calls an AWS API that R2 does not implement — so that has to be short-circuited too, or every settings save hangs and then fails.

The permalink trap

Worth knowing before you lose an hour to it: a fresh install has an empty permalink structure, and the pretty /wp-json/ routes do not exist until you set one. Hit the REST API before that and you get a 301 into an HTML page, which looks exactly like a broken deployment.

The diagnostic is /?rest_route=/wp/v2/posts. If that returns JSON, your API is fine and your rewrite rules are not.

Was it worth it

Honestly: for a client site with a content team, no. Managed WordPress hosting is cheap and someone else carries the pager.

For a personal CMS that feeds a static frontend, it is the right shape. The frontend fetches at build time, so the CMS could be down for an hour and no visitor would notice. Cold starts land around 800ms, which nobody experiences because nobody visits the CMS except me.

The thing I did not expect was how much cruft falls away once WordPress stops rendering pages. No theme. No emoji polyfill. No oEmbed discovery. Turning off the frontend removed more attack surface than any hardening plugin I have installed.

cloudflare-r2headlesssqlitevercelwordpress

Keep reading