Assignment moved into your own code: one bucketing contract across services, exposures logged at decision, and designs that hold under interference.
About this service
Moving assignment into your own code removes the anti-flicker snippet and the 300 to 700 ms of hidden page it costs a mobile visitor. That is the visible reason to do it. The real one is that everything worth testing on a marketplace or a gambling product happens after the request leaves the browser: ranking, matching, pricing, bonus logic, payment routing. None of it is reachable from a tag.
The bucketing contract:
One rule, written once, implemented everywhere. A hash of the unit id concatenated with an experiment salt, taken modulo 10000, deterministic and free of dependencies, so a Node service, a PHP monolith, a Go worker and a Swift client all place the same person in the same arm without consulting each other. This is the artefact the whole build stands on. It is a page of specification, it gets test vectors, and any service failing them is not allowed to log exposures. Where a team has skipped this and each service calls a vendor SDK independently, arms drift apart within days and nobody can say when it started.
Rendering:
Flags are evaluated during server rendering and serialized into the HTML payload, so the client hydrates from what the server already decided and makes no network call before painting. No flicker, no flag-fetch on the critical path, and a page that behaves the same for a user with an ad blocker as for one without, which quietly matters more than it sounds.
At the edge:
Cloudflare Workers or Fastly Compute where the page is cached, with the variant in the cache key. The cost is fragmentation, and it is arithmetic rather than opinion: a four-arm test on a surface already carrying three locale variants means twelve cached objects instead of three, and hit rate falls accordingly. I cap concurrent experiments per cached surface for that reason and will refuse an edge test where the origin cannot absorb the miss rate. Assignment stickiness comes from a cookie set by your origin in the response header, not written by script, so Safari's seven-day cap does not reshuffle returning users into new arms.
Backend experiments and interference:
This is where most server-side programs quietly produce nothing. In a marketplace, users are not independent: a ranking change that gives one seller more impressions takes them from another, so randomizing by user measures the treatment against a control the treatment has already contaminated. Those tests get switchback designs by city-week or cluster randomization by category, with the unit of analysis matching the unit of randomization. The same applies to bonus and pricing logic against a shared liquidity or budget pool. I specify the design before any code is written, and where the volume cannot support a valid design I say the test cannot be run rather than run an invalid one.
Exposure and flag debt:
Exposures are logged server side at the moment of decision, batched through Kafka or Pub/Sub into the warehouse, never fired from the browser where an ad blocker decides your sample. And every flag ages: past 90 days without an owner it is a branch nobody has read. I leave a removal queue and open the first pull requests myself, because a codebase carrying forty stale flags is how server-side testing gets banned internally.
Refusals:
I do not build a bespoke platform where self-hosted GrowthBook covers it, and I will say so even when a platform is what was budgeted. No price tests without written legal sign-off. No feature-exposure experiments on accounts belonging to minors. No backend test where the decision point cannot be logged, because an experiment you cannot attribute is a deployment with extra steps.
Wrong fit:
Teams with no engineer available to review pull requests weekly. This is code in your repository and it needs an owner on your side from day one.