DEV Community

Federico Corradi
Federico Corradi

Posted on • Originally published at fredcorr.com

Headless Isn't the Decision. The Trade-off Is.

Every architecture solves something and costs something. Headless is no different. The question is whether you're paying for the right thing.

I say this as someone who reaches for headless more often than not. I'm not here to talk anyone out of it. But I've watched enough teams adopt it for the wrong reasons, because it sounded modular, because a vendor demo was convincing, because it's what serious teams are supposed to do now, that I've come to think the architecture takes the blame for decisions that were never really made. The failures people call "headless problems" are almost always something else wearing its clothes.

So it's worth being precise about what headless actually buys you, and what it charges you for. Both are real. Neither is universal.

What it's genuinely good at

The core move in headless is decoupling the source of content from whoever consumes it. One source, many consumers, and none of those consumers reaching into the source to bend it to their needs. That sounds abstract until you've had to serve the same content to a website and a native app and realised that in a coupled system you're either duplicating the content or contorting one channel to fit the other. Headless makes that a non-problem. The content sits in one place; each consumer takes what it needs and shapes it however it likes.

Put a query layer in front of that source, a gateway with GraphQL, say, and you've made the backend-for-frontend pattern possible without heroics. Each consumer asks for exactly the data it wants, in the shape it wants. This isn't a workaround; it's roughly what GraphQL was invented for. When it's done well, a single source can feed a website, an app, a kiosk, and a partner API without any of them knowing or caring about the others.

The decoupling pays off in less obvious places too. Migrations stop being terrifying. Because the consumer talks to a contract rather than to the source directly, you can stand up a new CMS alongside the old one, build against it in a lower environment, test it properly, and switch over when it's actually ready, not in a big-bang cutover you've been dreading for a quarter. And because each consumer is independent, you can test and optimise them separately: try something on the app first, measure it, then decide whether the website gets it too. That independence is genuinely valuable, and it's hard to get any other way.

None of this is marketing. It's the upside, and it's real.

What it charges you for

Here's where teams get surprised, and where I've done my own share of learning the hard way.

The moment your interface needs two pieces of data that live on two different services, you're making two calls. If those calls are independent, fine: Promise.allfetch them together, move on. This is the case people picture when they imagine headless, and it's the case that works. But the interesting UIs are rarely that tidy. The moment call B depends on the result of call A, when you need the first response to know what to ask for in the second, parallelism can't save you. You've got a waterfall, and no amount of clever fetching flattens it. The complexity you thought you removed from the backend didn't disappear. It moved into the client, and now every consumer re-implements the same orchestration. That's not decoupling. That's a distributed monolith with worse latency.

"Fine," someone says, "put an aggregation layer in front of it." And they're right, that's usually the fix. But an aggregation layer is a thing you build, own, and maintain. It doesn't materialise for free because you drew it on a diagram. GraphQL gives you the clean per-consumer contract, but your team writes and maintains those queries, and CDN-level caching gets harder because every request is now a POST to the same endpoint rather than a cacheable GET. The gateway is the right call often enough, but it's a standing cost, not a one-off, and pretending otherwise is how the "headless made us slower" stories start.

Which brings me to the move I'd push back on hardest: reaching for a CDN to make the waterfall go away. The page is slow because it's making three calls to three services, so someone puts a CDN in front of the APIs and the numbers improve. Problem solved, apparently. It isn't. A CDN is not a delivery mechanism; it's a cache that happens to live at the edge. What you've actually done is trade a performance problem for a coherence problem, and coherence problems are worse, because they don't show up in a Lighthouse score.

Here's the mechanics. The API's CDN has a TTL, say 120 seconds. Your page revalidates on its own interval, say 60. Nobody chose those two numbers together; they were set by different people solving different problems, and nothing reconciles them. So the page regenerates on its clock and fetches whatever the edge is holding on its clock, and the two never line up. Next.js documents this plainly for its own stack: on-demand revalidation invalidates the server cache, but the CDN keeps serving its cached copy until the s-maxage TTL expires unless you also purge it yourself. Put the app on Kubernetes and it gets worse, because each replica's clock starts when that pod booted, so it isn't one desync, it's one per pod, permanently out of phase. Strapi wrote up exactly this failure: one instance regenerates, the others keep serving the old version, and their content editors stopped trusting the publish button entirely.

None of this is a bug, though. As one engineer put it while debugging stale content across layers, sometimes outdated content isn't a bug, it's policy: if you tell a system to hold data for an hour, it will. The staleness isn't a defect you fix. It's the shape of what you built. And the general principle underneath is simple enough to state in a sentence: most stale-content bugs trace back to two layers both thinking they own the same value. Managed platforms can solve this. Vercel purges HTML and data payloads atomically across regions, because they've priced the coordination in for you. The moment you add a cache the framework doesn't know about, that guarantee is yours to maintain. And if you can't say how stale a given page can be, not the ideal case, the worst one, you don't have a caching strategy. You have a hope.

The pattern underneath all three is the same. Headless doesn't make complexity vanish. It moves it. Done well, it moves it somewhere you can manage: a clear gateway contract, a bounded caching strategy. Done carelessly, it moves it onto the client, or into a tangle of invalidation rules, and then gets blamed for the mess. The analyses of real enterprise migrations bear this out: they run meaningfully more expensive and slower to learn than teams budget for, not because the tech is bad, but because the coordination it demands was never priced in.

The bet, made on purpose

On one publishing platform I worked on, we deliberately ran two sources: a headless CMS for editorial content and a separate GraphQL gateway for job and application data, because the CMS couldn't do the filtering the job board needed. Two API layers, not one. That sounds like exactly the sprawl I've just warned against, except it wasn't. The two weren't interdependent, so the calls ran in parallel, and the CMS request was thin scaffolding rather than blocking data. We accepted a cost because we understood it and had bounded it. That's the whole game.

Because here's what's actually changing. The tooling around headless keeps getting better: gateways, mature BFF patterns, edge compute, CMS platforms that ship proper preview and targeted invalidation out of the box. The technical barrier to adopting headless is falling every year. Which means the tool is no longer the hard part. Soon it'll be almost trivial to stand the whole thing up.

And that's precisely why the decision matters more, not less. When the tooling stops protecting you from difficulty, the only thing left protecting you from a bad call is whether you asked the right question before you started. Not "is headless better?", that question has no answer. The one that does: what, specifically, am I decoupling, and what am I willing to pay to break that coupling? If you can name both, headless is probably the right bet, and you'll make it with your eyes open. If you can only name the first, the upside, the modularity, the flexibility everyone talks about, then you haven't decided anything. You've followed a trend, and the invoice comes either way.

Headless isn't a default and it isn't a mistake. It's a bet. Make it on purpose, or don't make it.

Top comments (0)