Most writing about personalization is about tactics: which recommendation engine, which testing tool, which tag manager. This is about the layer underneath all of it, the architecture. Because personalization is not really a feature. It is a data-delivery problem wearing a product costume.
A personalized page is one where the bytes differ from one visitor to the next, computed from data about who they are, what they have done, and what is true right now: this shopper's cart, this SKU's live inventory, this segment's price. Serving that means some piece of compute has to look at fresh data and decide, per request, what to render. Every architectural choice below is a different answer to one question: how far is that compute from the fresh data it needs?
This article shares the five real answers, scores them on the things that matter for a personalized page at scale, and, where the question is measurable, settles it with a benchmark that built the same app twice.
Personalization is a data-delivery problem
There are two axes that decide everything, and both are the same currency: distance. Network distance is what a personalized page pays for in latency, and the two axes are simply the two distances that make up the bill.
The first is the distance from the user to the compute where the per-user decision actually gets made. You can bake it in ahead of time, and run it at the CDN edge, run it at an origin server, or run it in the browser. Those are the four places teams already build, and pushing the compute toward the user shrinks this first distance.
And there is never just one user. Picture five thousand users in five thousand locations, each a different distance from wherever your compute runs. So this first distance is not a single number; it is a spread, fast for someone beside your region, slow for someone an ocean away. An architecture is judged by how tightly it holds that spread across every user (consider the delta between P50 and P95).
The second axis is the distance from that compute to the fresh data it needs, and it is the one that quietly decides the outcome at scale. Wherever the compute lands, it still has to reach live data, and every hop it makes to do that shows up as latency, staleness, and cost. The trap is that the two distances pull against each other: move the compute toward the user and you usually move it away from the data.

How to read this comparison
To keep the five comparable, each is scored against the same job: a personalized product page for a large retailer, with per-user recommendations, live inventory and price, segment merchandising, served to users on both coasts under real traffic. It is read-heavy and freshness-sensitive, the kind of workload where stale or slow data does not just look bad, it breaks the product.
Each architecture is scored on eight dimensions:
- Freshness — can it serve up-to-the-moment data without staleness?
- Latency of the personalized view — how fast the personalized result reaches the user, including any flicker or waterfall.
- Personalization depth — segment-level only, or true one-to-one with live inputs?
- Operational simplicity — how many systems, bills, and failure modes there are, and how large the blast radius is.
- Cost at volume — the unit economics of a read once you are doing billions of them.
- Crawler visibility — whether the rendered result is visible to AI and Google bots. This matters for web presence and can impact revenue potential.
- Elasticity — how it absorbs load: instant autoscaling, or scale-out by adding capacity?
- Maturity — how proven and well-staffed the approach is, and the adoption risk of a newer one.
The opening comparison plots two of these eight directly, with latency as the height of each route and freshness as the node's color, because together they capture the sharpest part of the tradeoff.
Every architecture does well on at least one of these fronts; if it did not, it would not exist. The question is which one wins the combination this workload needs, which is fresh and fast and cheap and simple at the same time.
A note on the numbers. Where a claim is measurable, it comes from an open, reproducible benchmark that built the identical app twice behind one shared data contract: once on a unified runtime, and once on the common assembled stack (serverless functions calling a separate managed database, with a cache alongside it). Same UI, same code paths, same data; the only variable is the architecture. Of everything it tested, one scenario is squarely about personalization, a per-user read that cannot be cached, and that is the one this article leans on.
1 — Static variant generation
Resolves at build time, before the request exists
Decide the variance ahead of time. Segment your audience, render a page per segment (or per-segment fragments) at build, and push those variants to the CDN. At request time, there is no decision or data lookup; the edge simply serves the variant that matches the visitor. For the right content, this is still the best answer there is.

Where it wins. Nothing beats a cache hit. Reads are effectively free at any volume, delivery is edge-fast everywhere, and the fully rendered HTML is perfectly crawlable. For marketing pages, docs, or anything that changes on an editorial cadence, stop here.
Where it breaks. The data is frozen at build. Live inventory, price, and this-session behavior are impossible without a rebuild, which is fatal for a product page. And true one-to-one personalization means a combinatorial explosion of variants you cannot pre-render.
2 — Edge personalization
Resolves at the CDN edge, during the request, near the user
Run the personalization logic at the edge. A serverless function executes at the CDN point of presence nearest the visitor, close enough that reaching the compute is fast. But that function is stateless and the data still lives at the origin, so to personalize anything it calls back to the origin data tier. You have moved the compute close to the user, which also moves it further from the data.

Where it wins. For logic that needs to run close to the user and does not need much data — routing, geo and language, device class, A/B bucketing, header and cookie rewrites — the edge is ideal, and it renders server-side so crawlers see the result.
Where it breaks. The data is still at the origin. Every personalized read is a hop from the edge all the way back to origin, which is the longest data hop of any option here, so a data-heavy personalized page can end up slower at the edge than it would be at the origin. Edge runtimes are also constrained in memory and execution time.
3 — Server-side render with a separate data tier
Resolves at the origin server, per request, with the data in a separate service alongside it
The default shape of the modern web app, and the one most teams actually run. A server or serverless function renders each request, and to get the data, it calls a database that runs as a separate service. The two usually sit in the same region, often the same data center, so the gap is not really about geography. It is that the application and the data are two different systems, and every personalized read is a network call across the boundary between them, a connection and a round-trip paid on every read no matter how close the machines are.

This is the architecture the benchmark measured against the unified runtime. A single personalized read, the per-user value that cannot be cached, costs about 3 milliseconds as a network call to the database, against about 0.4 milliseconds when the same read happens in-process. End-to-end, a user in the database's home region gets that byte in about 34 milliseconds, and a user on the far coast about 94 milliseconds, because the read crosses the country to a single-region database. A real page is not one read, so the cost is paid again for every personalized element on the page.
Where it wins. Full freshness and unlimited depth: the database can answer anything, live, per request, and it is fully server-rendered, so crawlers see everything. This is probably the most mature architecture.
Where it breaks. The application and the database are separate systems talking over the network, so every personalized read is a call across that boundary. On a page with many personalized elements, the cost compounds, and a user far from the data region pays it in full. And when a spike hits, the compute may autoscale, but the shared database is the ceiling it all funnels into.
4 — Client-side personalization
Resolves in the browser, after the page has loaded
Ship a generic, cacheable page fast, then let the browser personalize it. After load, client JavaScript calls an API for this user's data and swaps in the personalized parts. It is the easiest thing to bolt onto an existing site, which is why it is everywhere, and it puts the deciding compute as far from the fresh data as it can possibly get.

Where it wins. The base page is static and CDN-fast, so first paint is quick, and it is the simplest option to add to a site you do not want to re-architect. For personalization that is genuinely optional decoration, such as welcome-back strips or recently-viewed rails, it is fine.
Where it breaks. A visible waterfall and a flash of unpersonalized content, because the personalized parts arrive after load. Crawlers never see the personalization at all. And it does not remove the remote data tier; it sits on top of it, inheriting all the costs of the previous architecture and adding a client round-trip.
All four are fighting the same fight
Static freezes the data to avoid the hop. Edge moves the compute out to the user, but leaves the data at origin, so it still hops back for every read. Server centralizes near the data, then hops per read to use it. Client flees as far from the data as possible and hops twice to get back. Everyone is a different accommodation of the same fact: the compute that decides is somewhere other than where the fresh data is. The fifth architecture refuses to accept these norms.
5 — Unified untime
Resolves in-process, where the compute shares the same memory space as the data
Stop moving data to compute or compute to data, and make them the same thing. Here, the application logic runs inside the database process, in the same memory space as the data. A personalized read is not a network request to another service; it is a function call against an in-memory table. Caching is in-process. The whole unit is then replicated to a node in every region and the copies are kept globally in sync, so each user is served in-process by the nearest node and still sees the same data. There is no internal hop to remove, because there was never an internal boundary.

In the benchmark, the same personalized read that costs about 3 milliseconds as a hop to Postgres is about 0.4 milliseconds as an in-process call, roughly eight times faster, and geography-neutral because there is no network in it. End to end, the user's personalized byte arrives in about 11 to 13 milliseconds on both coasts, against 34 in-region and 94 cross-country for the assembled stack. The realistic hybrid tells the same story: cache the non-personalized shell on the CDN, and serve the personalized value from the runtime in-process, so you keep the CDN's speed for the static frame and drop the personalized read from roughly 3 milliseconds to sub-millisecond.
Where it wins. The only architecture that is fresh, fast, cheap, and simple at once: live in-process reads, edge-grade latency on both coasts, full one-to-one depth, one system with one bill, deep multi-region redundancy, and solid unit economics because the whole system is extremely memory-efficient. It is fully server-rendered, so crawlers see everything.
Where to be honest. It does not add capacity in an instant, the way stateless serverless compute does; a single node can scale vertically to handle massive load, but ultimately caps out. Scaling out across regions is typical for scaling reads. It is also a newer pattern than a multi-system stack.
The comparison
Scored for the workload we started with: a personalized product page at scale, where freshness, latency, and cost all bind at once.

Read it by constraint, not as a trophy. If your personalization is a handful of segment or geo swaps, Edge is a good option. If your pages are essentially editorial, Static variants are unbeatable. If you are bolting light personalization onto a site you cannot touch, client-side is pragmatic. But for fresh, deep, per-user data delivered fast and served at scale under cost pressure, no column is flawless, and one is clearly strongest. The unified runtime holds every core dimension. Where it gives ground is narrow: it is a newer platform than a typical database-and-CDN stack, and it is not instantly elastic — though no architecture serving live data really is, since the compute always scales faster than the shared data tier behind it. Those are tradeoffs a team can plan around; the other four give ground on the very things this workload cannot compromise.
Closing the gap
Four of these architectures are strategies for living with the distance between compute and fresh data. They freeze it, move the compute to the edge, centralize-and-hop, or flee from it, and each strategy shows up in the scorecard as a specific, predictable weakness. The fifth does not manage the distance; it removes it by making the data the compute's own memory and then putting that unit near the user. That is why it is the only one that holds freshness, latency, depth, cost, and simplicity together, and why the eight-times-faster read is not a trick, but the single consequence of a clever decision.
If you have built with opinionated, convention-over-configuration frameworks, the instinct is familiar. That approach is one reason Rails and Spring won their eras: one runtime, sensible defaults, and the data and logic living together instead of being wired across a network. The unified runtime is that instinct applied to the data path. The honest scope holds: a CDN still wins the cacheable shell, a single node has a ceiling you scale past by adding nodes, and it is a newer bet. The claim is not that co-location wins everything. It is that it wins the part that is actually hard: live, personalized data at scale.
See the whole thing and check the work: the benchmark builds the same app twice and publishes every number, caveat, and method, including where the unified runtime loses.
The benchmark: github.com/HarperFast/harper-vs-vercel-benchmark
Build with a Unified Runtime: www.fabric.harper.fast






.webp)



.jpg)
