Headless WordPress: when it is warranted, when it is not, and what ISR actually costs you
I get asked some version of this every few weeks: "should we go headless with WordPress?" The honest answer is that headless is a trade, not an upgrade. You give up a large amount of convenience and you buy back control over rendering, caching, and the public attack surface. Whether that trade is worth it depends on things most vendor pitches never mention.
I run both sides of this. Champlin Enterprises runs on headless WordPress: content lives in WordPress, it is queried over WPGraphQL, and the public site is a Next.js App Router build on Vercel with tag-based Incremental Static Regeneration. I also maintain a fleet of ordinary WordPress sites on Plesk where going headless would have been pure cost with no return. Neither of those is the "right" architecture. They are answers to different questions.
What headless actually buys you
Strip out the marketing and there are three real benefits.
- You control the rendering layer. No theme, no plugin filters injecting markup, no
wp_headsurprises. What ships to the browser is what you wrote. If you have ever tried to hit a Core Web Vitals target while three plugins fight over jQuery, this is the whole pitch. - PHP stops executing on the public edge. Visitors hit static or cached output on a CDN. Plugin code is no longer in the request path for anonymous traffic. That is a genuine reduction in exploitable surface, and it is the security argument worth making.
- Content becomes reusable. One WordPress install can feed a website, a mobile app, an email system, and a partner feed. If you genuinely have more than one consumer, the API layer stops being overhead and starts being the point.
Notice what is not on that list. Headless does not make WordPress faster. It does not make your content model better. It does not fix a slow database or a bloated plugin stack. It relocates those problems to a place where they are less visible to visitors, which is not the same as solving them.
When headless is warranted
I look for these signals. Two or more and the conversation is worth having. One on its own usually is not.
1. There is a second consumer of the content
A mobile app, a kiosk, a partner syndication feed, a second brand site sharing a content library. This is the strongest signal, because the API is going to exist either way. Once you are maintaining a content API, rendering the marketing site from it too is marginal work rather than a new system.
2. The front end has real application behavior
Configurators, dashboards, authenticated portals, complex filtered search, anything with meaningful client state. Building that inside a theme means fighting the theme. Building it in a modern framework and pulling content over an API means the CMS does what it is good at and stops being in the way.
3. The performance target is genuinely tight
If a real business number moves with a few hundred milliseconds, and you have already exhausted the cheap wins, then owning the rendering path matters. Be honest about "already exhausted", because most sites have not.
4. You have a front-end engineer, not just an agency retainer
This is the one that quietly decides most projects. A headless site is a build pipeline, a deploy target, a dependency tree, and a cache invalidation contract. Someone has to own that on the Tuesday morning it breaks. If the answer is "our agency, when we file a ticket", headless will feel like an outage generator.
5. Editors do not need pixel-accurate live preview
If the editorial team works in structured fields and trusts the design system, headless is fine. If they compose visually in the block editor and expect what they see to be what ships, headless will feel like a downgrade from day one, because it is one.
When headless is not warranted, which is most of the time
I will talk clients out of it in these cases, and I have.
- Brochure and local service sites. Ten to forty pages, a contact form, a blog nobody updates weekly. A well-built theme, a real host, and full-page caching will beat a headless rebuild on every metric including speed, because there is nothing here that headless is good at.
- WooCommerce. Headless commerce on Woo is a serious build. Cart, checkout, taxes, shipping rules, payment gateways, and every extension you own assume they are rendering in PHP. You can do it. You should be very sure why.
- Plugin-dependent workflows. Form builders, membership gating, event calendars, redirect managers, popups. Each one you rely on is a thing you now reimplement or bridge. Count them before you commit, not after.
- Small teams with no deploy culture. If nobody owns CI, nobody will notice a failed build. Which brings us to the part that actually goes wrong.
ISR, and the three ways it bites
Incremental Static Regeneration is the mechanism that makes headless viable at reasonable cost. The idea is simple: render a page once, serve that cached copy to everyone, and regenerate it in the background either on a timer or when something tells you the content changed. You get static-file performance with content that still updates.
Use ISR when content changes on human timescales: marketing pages, blog posts, service and project listings, anything you edit a few times a week. Do not use it for anything per-user or genuinely real time. Personalized dashboards, cart state, live inventory, and authenticated views want server rendering or a client fetch, because a shared cache entry is the wrong shape for data that differs per visitor.
The setup I run: a sensible time-based fallback (an hour is fine for most marketing content) plus on-demand revalidation by cache tag, fired from WordPress when a post is saved. The timer is the safety net. The webhook is what makes edits feel instant. Here is what goes wrong with that arrangement, all three of which I have hit in production.
The webhook secret drifts and nothing tells you
The revalidation endpoint is authenticated with a shared secret. If the value in WordPress and the value in the hosting environment fall out of sync, every webhook returns 401 and the site keeps working perfectly. Content just stops appearing until the hourly timer catches it. There is no error page, no alert, and no obvious symptom other than editors saying "it takes a while to show up".
I lost an unknown number of days to exactly this. The fix is trivial once you know. The lesson is that the first thing you check when a headless site feels stale is secret parity, not caching logic. Better: make the endpoint log rejected calls somewhere you actually read, and treat a run of 401s as an alert rather than a curiosity.
Not every content change fires the hook you think it does
WordPress hooks are more specific than they look. Updating post meta directly, for example through WP-CLI, does not fire save_post. So a custom-field-only edit sails through without ever triggering revalidation. The page keeps serving the old value until the timer expires, and you go hunting through cache code for a bug that is not there.
The pattern that works: revalidate on an explicit signal you control, not on an inferred one. Either follow a meta update with a real post update, or call the revalidation endpoint directly with the right tag as part of whatever changed the data.
Stale-if-error can hide a broken build forever
This is the nastiest one. Most static regeneration setups will serve the last good cached copy if regeneration throws. That behavior is a gift during a brief upstream blip and a trap otherwise, because a page whose regeneration crashes every single time will serve stale content indefinitely, with a 200 status, looking completely healthy to any uptime monitor.
A 200 does not mean the page is current. Monitor for the thing that actually matters: does the rendered HTML contain content you know is recent. Watch regeneration failures as a first-class signal, not just HTTP status.
Performance: where the wins really come from
The uncomfortable truth is that most WordPress performance problems are not architecture problems. Before anyone rebuilds anything, this list is cheaper and usually enough:
- Full-page caching at the edge. An anonymous visitor should never wake up PHP. This alone is most of the gap between a slow WordPress site and a fast one.
- Object caching. Redis or Memcached in front of the options table and repeated queries.
- Current PHP. PHP 8.3 is dramatically faster than what a lot of sites are still running.
- Fewer plugins, and no plugin that adds a request to a third party in the render path.
- Images and fonts. Modern formats, correct dimensions, self-hosted fonts, explicit width and height so nothing shifts.
- A host that is not oversold. No amount of front-end work fixes a 900ms time to first byte on shared hosting.
Do that and a conventional WordPress site is genuinely fast. What headless adds on top is consistency and a ceiling: the cheap tricks have a floor you eventually hit, and past that point owning the rendering layer is how you keep going. But if you skip the list above and go headless, you will build a fast shell around a slow origin and be confused about why editors wait eight seconds for the admin to load. The origin does not get faster because visitors stopped seeing it.
Security: what changes and what does not
Headless is often sold as a security upgrade. Part of that is true and part of it is wishful.
What genuinely improves: anonymous traffic no longer executes plugin PHP. The classes of vulnerability that dominate WordPress incident reports, unauthenticated plugin exploits reachable from a public page, largely stop being reachable through the front door. That is real, and it is the reason I am comfortable running a public-facing headless setup.
What does not change: the WordPress install is still there, still on the internet, still running the same plugins. It has a login page, an admin surface, and an update treadmill. All you have done is reduce its audience. Lock it behind an IP allowlist or a VPN if you can, keep it patched exactly as before, and put real multi-factor authentication on it. A neglected headless CMS is a neglected WordPress site with better hiding.
What gets newly dangerous: the API layer is now the product, and APIs leak differently than themes do.
- Registering a post type as non-public does not keep it out of the REST API. REST exposure is governed by its own settings, so a "hidden" content type can still be enumerable over the API. Check what your endpoints actually return, from a logged-out session, rather than assuming a flag covered you.
- Draft and unpublished content. Preview mechanisms need to be authenticated on their own terms. It is easy to ship a preview route that will happily render anything given an ID.
- Query cost. A public GraphQL endpoint without depth or complexity limits is a denial of service waiting for someone bored. Limit it, and put it behind a cache.
- Secrets in two places. Revalidation secrets, API keys, and webhook tokens now live in both systems and drift silently, as covered above. Rotate them deliberately and verify both sides after every rotation.
The short version
Go headless when you have a second consumer for the content, real application behavior on the front end, a performance target you have already earned the right to chase, and someone whose job includes owning a build pipeline. Stay conventional when you have a content site, a plugin-shaped workflow, editors who compose visually, or a team that does not deploy software.
If you do go headless, treat cache invalidation as a contract rather than a configuration detail. Know which signal revalidates which content, verify that the signal actually fires, and monitor rendered output instead of status codes. That is the part that decides whether the trade was worth it, and it is the part almost nobody plans for.
The best architecture is the one your team can still operate correctly at 4pm on a Friday. Sometimes that is headless. Frequently it is not, and saying so out loud is worth more to a client than another rebuild.