← Blog

Server-side A/B testing without a network call

Server-side A/B testing assigns a user’s variant on the server, before the page renders, instead of rewriting the page in the browser with a script that loads after. The variant is decided in the same process that renders the HTML, so there’s nothing to flicker and no round trip to a separate service to ask which version a user gets.

Most A/B testing tools do the opposite. A visual editor injects a script tag, waits for it to load, then rewrites the DOM after the original page has already painted. The flash of the old content before the new one appears half a second later is the tell. For a payment flow or a pricing page, that half second isn’t cosmetic: it skews click data and ships a visibly broken page to anyone whose ad blocker caught the script.

This post covers how server-side A/B testing works without a network call, how dif assigns a variant deterministically, and the one real cost of giving up the visual editor.

Key Takeaways

  • Server-side A/B testing decides a user’s variant on the server, or at the edge, before the page renders, so there’s no flash of the control variant and no client-side script to load.
  • “No network call” means the variant comes from a pure function of the user id, salt, and weights, not a request to an assignment service.
  • dif computes the first four bytes of SHA-256(salt || user_id) mod 10,000 and reads the variant off cumulative weights. Nothing is stored.
  • Server-side assignment means no point-and-click visual editor. Whoever writes the experiment edits a file, not a live page.
  • A per-experiment salt keeps a user’s bucket in one test independent of their bucket in another.

Client-side testing flickers. Server-side doesn’t.

A client-side A/B testing tool works by waiting. The browser loads the page, then the test’s script tag, and then the script rewrites whatever element the test targets. Between those two renders, the user sees the control variant for a few hundred milliseconds, then watches it change. That’s the flash of original content, and it shows up as layout shift the moment the swap happens, the same thing the Cumulative Layout Shift spec was written to measure.

Server-side A/B testing skips the wait. The variant is decided before a single byte of HTML leaves the server, so the page that reaches the browser is already the right one. There’s no flicker to hide, because there was never a wrong version rendered first.

It also sidesteps a quieter problem. Ad blockers and privacy extensions routinely block third-party test scripts. A client-side test silently drops those users from the experiment, or worse, shows them a half-applied variant. A server-side test has no script to block.

What “no network call” actually means

The decision is computed entirely from data already in the request: the user id and the experiment’s salt, hashed and mapped to a variant. No separate assignment service, no added latency waiting on one. dif computes a user’s bucket as the first four bytes of SHA-256(salt || user_id), read as an integer, mod 10,000, then maps that number onto the experiment’s cumulative weights.

bucket = int(sha256(salt + user_id)[:4]) % 10000

A 50/50 test paints buckets 0 to 4,999 control and 5,000 to 9,999 the variant. Because the hash is deterministic, the same user id and the same salt always produce the same bucket every time. Nothing is written to a database first, and nothing is read from one on the next request. This is the same deterministic model behind experimentation in dif: a flag and an A/B test are assigned the same way, the only difference is the weights and whether there’s a hypothesis.

A per-experiment salt keeps two concurrent tests from correlating. Being in the 10% for one rollout says nothing about which bucket you land in on an unrelated pricing test, because each test hashes against its own salt.

Where server-side assignment happens

Where in the request lifecycle assignment happens is the other half of “server-side.” Decide it in a server-rendering framework’s load function or middleware, before the template renders, and the variant is baked into the first response.

// src/routes/pricing/+page.server.ts
export async function load({ cookies }) {
  const userId = cookies.get('uid');
  const variant = dif('pricing-cta-copy', { userId });
  return { variant };
}

The failure mode to watch for is a mismatch between what the server decided and what the client re-renders. A framework that hydrates on the client has to receive the same variant the server picked, or React throws a hydration mismatch and re-renders from scratch, the exact client-side flicker you were trying to avoid. Passing the assigned variant down as server-rendered data, not recomputing it in the browser, is what keeps the two in sync.

The tradeoff: no visual editor

Server-side A/B testing costs you the thing a marketer liked about the visual editor: anyone could point, click, and launch a test without a deploy. In dif, starting a test means writing a Markdown file and opening a pull request.

For a payment flow or a pricing page, where a changed sentence has to match a database field or a webhook, that’s a fair trade. A growth PM who needs to retest ad copy on a landing page twenty times a week might reasonably want the point-and-click editor back for that surface. Know which kind of test you’re running before you give it up.

Setting it up

Server-side assignment needs three pieces: the experiment file, the call site, and a render path that passes the result down instead of recomputing it.

npm install -g @dif.sh/cli
dif init
dif new

dif new scaffolds an experiment file in dif/experiments/:

---
id: pricing-cta-copy
status: active
owner: sam@acme.com
surface: pricing
hypothesis: >
  A benefit-led CTA ("Start building") lifts trial signups
  over the control ("Get started").
variants:
  - id: control
    weight: 50
  - id: start_building
    weight: 50
metrics:
  primary: trial_signup
  guardrails:
    - support_tickets
---

Run dif validate before the PR merges, so a weight that doesn’t total 100 or a typo’d surface name fails CI instead of production:

$ dif validate
✓ pricing-cta-copy: weights sum to 100, owner valid, surface exists

The CLI reference covers every check it runs. dif build compiles the experiment into the typed client your server load function calls, and the hash itself runs through Node’s crypto module.

The SDK docs cover the server-load adapters for React and SvelteKit, and the first experiment guide walks the whole loop end to end.

FAQ

What is server-side A/B testing? Assigning a user’s variant on the server, before the page renders, rather than rewriting the page in the browser with a script after it loads. The response the browser receives is already the correct variant.

Is server-side A/B testing faster than client-side? Yes, for the user. There’s no script to download and execute before the page looks right, and no flash of the control variant while that script runs. The server does the same work a client-side editor would, just before the response leaves instead of after it arrives.

Do I need a database to assign variants server-side? No, if assignment is deterministic. Hashing a user id with a per-experiment salt produces the same bucket every time, so nothing needs to be stored or looked up on the next request.

Can I run server-side A/B testing without full server-side rendering? Yes. An API route, a middleware layer, or an edge function can assign a variant and return it in the response body or a cookie. The requirement is deciding before the client renders, not a specific rendering architecture.

What about ad blockers? Server-side tests have no third-party script for an ad blocker to catch, so you don’t silently lose a chunk of users from the experiment the way a client-side visual editor does.

Conclusion

Server-side A/B testing means deciding the variant before you render: on the server, or at the edge, with a deterministic hash instead of a stored lookup, then passing it down instead of recomputing it in the browser, where a mismatch causes the flicker you were trying to avoid.

The point-and-click editor goes away. In its place is a test that never flashes the wrong variant and lives in the same pull request as the code it’s testing, with no third-party script for an ad blocker to catch.

If you’re already running experiments in code, the shape is the same one covered in A/B testing for developers: a hypothesis, a deterministic split, a tracked metric, a recorded decision. Install the CLI, scaffold an experiment, and assign it server-side from the start:

npm install -g @dif.sh/cli
dif init