Headless WordPress: When It Is Worth It and When It Is a Trap
When headless WordPress is worth the cost and when it’s a trap: what decoupling deletes, the real performance numbers, preview, revalidation and a decision
A client came to us in 2024 with a decoupled WordPress build that had cost them roughly £60k. Next.js front end, WPGraphQL, the whole stack. The complaint wasn’t performance. It was that their marketing lead couldn’t add a two-column section with a testimonial to a landing page without filing a ticket, and the ticket took nine days.
We measured the old site they’d replaced. Full page cache on a decent host, LCP around 1.4s on 4G. The new headless build: LCP around 1.3s. They paid £60k for 100 milliseconds and lost the ability to publish.
That’s the failure mode nobody writes about. Headless WordPress is a genuinely good architecture for a specific set of problems, and a very expensive way to make a brochure site slightly faster. The hard part is telling which one you have before you sign the contract.
- Decoupling deletes the front-end output of every plugin you own: forms, SEO meta, schema, sitemaps, redirects, related posts, galleries, oEmbeds. Budget to rebuild each one, not to “port” it.
- Performance is rarely the real justification. A WordPress theme with a full page cache at the edge serves HTML in 30 to 80ms TTFB, which is the same ballpark as a statically generated front end.
- Headless earns its keep when WordPress is one content source among several, when the front end is a genuine application rather than pages, or when the CMS must sit on a private network.
- Preview and cache invalidation are where most builds break. Authenticated draft rendering and signed revalidation webhooks are not optional extras, they’re core infrastructure.
- Partial decoupling, where WordPress still renders HTML and React handles only interactive islands, gets you most of the upside for a fraction of the maintenance surface.
The features you silently delete
Everyone discussing decoupled WordPress talks about what you gain. Here’s the inventory of what stops working the moment your front end stops being a PHP theme.
- Forms. Contact Form 7, Gravity Forms and WPForms all render markup and post back through WordPress with nonces. None of that survives. You either hit their REST endpoints with hand-rolled fetch logic and lose the spam protection and conditional logic, or you swap to a form API. On static front ends we usually drop in WebForms because there’s no backend to maintain and submissions route to email, Slack or a webhook.
- SEO output. Yoast and Rank Math both ship REST surfaces now, so the title and description are retrievable. The schema graph, the breadcrumb markup, the canonical logic for paginated archives and the XML sitemap all have to be re-derived in your front end. Sitemaps in particular get forgotten until a crawl report comes back ugly.
- Redirects. Your redirect plugin writes rules that run on PHP requests nobody makes anymore. Export them to the host’s edge config and set up a sync, or accept 404s on every legacy URL.
- Blocks that render on the server. Core blocks like gallery, embed, navigation and query loop produce HTML through PHP render callbacks. The REST API gives you
content.rendered, which contains that HTML, so if you inject it you are shipping WordPress markup into React and styling it by hand. If you instead parse the block tree and re-implement each block as a component, you own a block library forever. - theme.json. All those design tokens stop applying. You rebuild them in your front end’s system and keep two sources of truth in sync. We wrote about what theme.json actually controls, and it’s more than most people assume.
- Comments, search, pagination, feeds, i18n routing. Each one is a half day to three days of work. They add up to weeks.
None of this is a reason not to go headless. It’s a reason to put every item on the estimate.

The performance argument is usually wrong
“WordPress is slow” nearly always means “this WordPress install is slow”, and the cause is almost never PHP. It’s 38 plugins, an uncached WPQuery with metaquery on a 200k row postmeta table, six render-blocking stylesheets and hero images served at 2400px wide.
Cache the HTML and the PHP stops mattering. A page served from a full page cache at the edge, whether that’s Cloudflare, a Varnish layer or your host’s own, lands in the 30 to 80ms TTFB range. A statically generated Next.js page from a CDN edge lands in a similar range. The difference is noise next to a 900KB unoptimised hero.
Where headless genuinely wins on performance is the second page view onwards: client-side routing, prefetched payloads, no full document parse. That matters enormously for a documentation site or an app-like interface where users traverse 15 screens. It matters almost nothing for a site where the median session is 1.4 pages and the entry point is organic search.
Before you decouple anything, run the diagnostics. Query Monitor will show you the slow queries, and in our experience the fix is usually three indexed meta lookups and an object cache, not a rewrite.
When headless WordPress earns its keep
There are four scenarios where we’ll recommend it without hesitation.
WordPress is one source among several
You’re building an app that pulls product data from Shopify, pricing from an internal service, docs from WordPress and support articles from Zendesk. The front end has to compose all of it. The CMS is a data source and nothing else, so a PHP theme is simply the wrong shape. This is the strongest case.
The front end is an application, not pages
Dashboards, configurators, booking flows with real state, anything with offline behaviour or a React Native sibling. If 80 percent of your front end is already a client-side app and the marketing pages are the remaining 20 percent, dragging a PHP theme along is the awkward option.
The CMS must be unreachable
Regulated clients, government work, anything where the security review says the authoring system cannot have a public login page. Put WordPress behind a VPN, build statically, ship HTML. The security sign-off alone can justify the architecture on that kind of engagement.
Content feeds surfaces that aren’t the web
Native apps, in-store displays, email generation, a partner API. If three or more consumers need structured content, you need an API layer regardless. At that point the website being another API consumer is consistent rather than exotic.
Notice what isn’t on the list: “we want a modern stack”, “React is easier to hire for”, “our Lighthouse score is 72”. The first two are real considerations but they’re team preferences, not architecture requirements, and they should be priced as such. The third is a caching problem.
Choosing the data layer
For a WordPress REST API frontend, the default endpoints get you surprisingly far if you stop requesting everything. A bare /wp/v2/posts call returns rendered content for all ten posts and can easily exceed 300KB. Trim it:
curl -s "https://cms.example.com/wp-json/wp/v2/posts?perpage=12&fields=id,slug,title.rendered,excerpt.rendered,date&_embed=wp:featuredmedia"
Same data you need for an archive grid, usually under 10KB. The _fields parameter is the single highest-leverage thing most teams miss.
Where REST hurts is relational data. Post, plus author, plus featured image in three sizes, plus two taxonomies, plus ACF relationship fields pointing at other posts: that becomes four to six round trips. WPGraphQL solves that with one query and a typed schema, which is why it’s still the sensible default for anything with nested content. The cost is another plugin in the critical path, a non-trivial caching story, and schema changes that break builds when someone renames an ACF field.
Our preference on client work: one custom REST namespace that returns exactly the payload each template needs, versioned and cached. Fewer requests, no over-fetching, and the contract is yours rather than WordPress’s. We covered the auth, caching and versioning mechanics in custom REST API endpoints in WordPress. The trade-off is obvious: you’re writing PHP for a build whose selling point was not writing PHP.
Preview and invalidation, the two things that break
Editors will judge the entire project on whether preview works. In a coupled theme it’s free. Decoupled, you need an authenticated request path: generate a short-lived token on previewpostlink, send the editor to a front-end preview route that fetches the draft with an application password or JWT, and make sure that route is never cached. Get this wrong and your drafts leak publicly, which happens more often than anyone admits.
Invalidation is the other half. Full rebuilds are fine at 200 pages and unusable at 20,000, so you want targeted revalidation fired from WordPress on save:
addaction( 'transitionpoststatus', 'scwrevalidateonchange', 10, 3 );
function scwrevalidateonchange( $newstatus, $old_status, $post ) {
// Ignore draft-to-draft churn; only publish transitions matter.
if ( 'publish' !== $newstatus && 'publish' !== $oldstatus ) {
return;
}
if ( wpispost_revision( $post ) ) {
return;
}
// Fire after the response so a slow front end never blocks wp-admin.
add_action( 'shutdown', function () use ( $post ) {
$body = wpjsonencode( [
'type' => $post->post_type,
'slug' => $post->post_name,
'eventid' => wpgenerate_uuid4(), // lets the receiver dedupe retries
] );
wpremotepost( SCWREVALIDATEURL, [
'timeout' => 5,
'blocking' => false,
'headers' => [
'Content-Type' => 'application/json',
'X-SCW-Signature' => hashhmac( 'sha256', $body, SCWREVALIDATE_SECRET ),
],
'body' => $body,
] );
} );
}
And the receiver, verifying the signature against the raw body:
import { revalidateTag } from 'next/cache';
import { createHmac, timingSafeEqual } from 'node:crypto';
export async function POST(req) {
const raw = await req.text(); // hash the raw body, never the parsed object
const sig = req.headers.get('x-scw-signature') ?? '';
const expected = createHmac('sha256', process.env.REVALIDATE_SECRET)
.update(raw).digest('hex');
if (sig.length !== expected.length ||
!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return new Response('bad signature', { status: 401 });
}
const { type, slug } = JSON.parse(raw);
revalidateTag(${type}:${slug});
revalidateTag(${type}:archive); // lists that embed this item
return Response.json({ ok: true });
}
Two things bite here. Retries mean the same event arrives twice, so dedupe on event_id; the patterns in our piece on webhook idempotency apply directly. And editing one post invalidates more surfaces than you think: the archive, the homepage block, the sitemap, the related-posts module, the category page. Map those relationships once, in code, or editors will keep reporting “my change isn’t showing”.
The middle path most teams should take
You can have React where React helps without decoupling the whole site. Keep WordPress rendering the document, mount components into specific containers, and hydrate only what needs state. A product filter, a seat picker, a live search box. Everything else stays server-rendered HTML that works before any JavaScript executes, which is the posture we argue for in progressive enhancement in a JavaScript world.
You keep preview, plugins, theme.json, redirects, sitemaps, forms and the block editor. You get a modern interactive layer for the 10 percent of the site that needs it. One deploy pipeline, one error log, one hosting bill.
This is the recommendation for maybe 70 percent of the projects that arrive asking for headless. It’s less interesting to build. It’s usually right.
The questions that settle it
Answer these honestly before anyone opens a terminal.
- How many non-web consumers of this content exist today? Zero is a strong signal against decoupling. Two or more is a strong signal for it.
- Who builds a new landing page in month 14? If the answer is a marketer, headless makes their job harder every single time.
- Can you fund two codebases for three years? On our projects a decoupled build adds roughly 30 to 50 percent to initial delivery and close to doubles the ongoing maintenance surface. That’s two dependency trees, two CI pipelines, two sets of security updates.
- Is there a front-end engineer on the retainer? Headless sites that lose their JavaScript developer become unmaintainable within a year. A PHP theme degrades much more gracefully.
- What’s the measured TTFB and LCP today, and what’s the target? If you can’t state both numbers, performance is not your actual reason.
- Does a security requirement force the CMS off the public internet? If yes, you’re done deliberating.
One more sanity check: run a full page cache in front of the existing site for two weeks and remeasure. We’ve had that single change close the entire performance gap a client was planning to spend six figures on.
Frequently Asked Questions
Is headless WordPress better for SEO?
No, it’s neutral at best and a liability if you’re careless. Googlebot renders JavaScript, but you lose automatic sitemaps, canonical handling for paginated archives and schema output, so you have to rebuild all of it correctly. Server-side render or statically generate every indexable route and verify with the URL Inspection tool rather than assuming.
REST API or WPGraphQL in 2026?
WPGraphQL if your content is deeply relational and you want a typed schema the front end can rely on. The REST API with _fields if your needs are shallow and you’d rather not add a plugin to the critical path. For most client builds we end up with a small custom REST namespace returning exactly the shape each template needs, which beats both on payload size and caching simplicity.
How do forms work on a decoupled front end?
Not by themselves. Your existing form plugin renders PHP markup and validates with nonces, neither of which exists anymore, so you either call its REST endpoints manually or move to a form API that accepts a POST and handles delivery, spam filtering and notifications. The second option is less work and fewer moving parts on a static front end.
Will going headless reduce hosting costs?
Usually not. The front end gets cheap or free, but WordPress still needs PHP, MySQL and backups, and now you’re also paying for build minutes, edge functions and a second monitoring setup. The real cost shift is from hosting to engineering time, and engineering time is more expensive.
Can we decouple just one section of the site?
Yes, and it’s often the smartest version. Serve the marketing pages from the WordPress theme and route a subpath like /app to a separate front end at the CDN or reverse proxy, sharing design tokens between the two. You keep editorial velocity where content changes weekly and get a real application stack where state is complex.
Make the call on evidence
Here’s the decision. If you can name two or more non-web consumers of your content, or a security requirement that forces the CMS off the public internet, or a front end that’s genuinely an application, go headless and budget seriously for preview, invalidation, redirects and forms from day one.
If you can’t, do this instead: put a full page cache in front of what you have, fix the slow queries, compress the images, and spend the remaining budget on content and conversion. Then remeasure in a fortnight. The number will tell you whether you ever had an architecture problem.

