> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trevosdk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Variant mismatch

> A user reports seeing the wrong version, or the same person gets different variants.

Someone says the interface changed under them, or two people on your team see different things
and expect not to, or your server and browser disagree.

Assignment is deterministic — the same identity and experiment key always produce the same
variant. So a mismatch usually means **the identity changed** — or the two sides are
reading different copies of the config.

## `identify()` was called after the variant rendered

The most common cause by far.

Before `identify()`, a visitor is bucketed on an anonymous id. After it, they're bucketed on
your user id. Different input, different hash, potentially a different variant — so a user who
logs in mid-session can watch the page change.

```ts theme={null}
trevo.init({ apiKey });
const variant = trevo.getVariant('checkout-cta');   // bucketed anonymously
// ...user logs in...
trevo.identify(user.id);                            // basis changes — variant may flip
```

**Fix:** call `identify()` as early as you can, before rendering anything under test. In React,
that means in an effect that runs as soon as auth resolves, not deep in the component tree.

It also skews data: the anonymous exposure and the identified conversion can end up attributed
to different arms. Identifying early is a correctness fix, not just a cosmetic one.

## The same person on two devices

Anonymous ids live in browser local storage, so they're **per browser**. The same human on a
laptop and a phone is two participants until they sign in, and can legitimately be in different
arms.

Once both sessions call `identify()` with the same user id, both bucket on that id and agree.
Nothing can retroactively merge the pre-login history on the second device.

Expected behaviour, worth explaining to whoever reported it.

## Server and browser disagree

Both sides must bucket on **the same identity at the same moment**:

* `userId` when the visitor is identified
* otherwise the `trevo_id` cookie value

A backend that buckets on `userId` while the browser is still anonymous will assign a different
variant to the same visit.

```ts theme={null}
const variant = trevo.getVariant('pricing-v2', {
  userId: req.user?.id,               // may be undefined
  anonymousId: req.cookies.trevo_id,  // always send it
});
```

Next.js `middleware` + `getTrevoBootstrap()` handle this for you. If you're hand-rolling with
`resolveExperiments()`, it's your responsibility.

## The config changed and one side is on the old copy

Assignment is deterministic in identity + experiment key + the variant list. Change a
traffic split or reorder variants and the sides disagree until caches turn over: the API
caches config for \~55s, the browser SDK polls every 60s and serves a localStorage copy up
to 24h old before its first fetch lands, and Next.js SSR revalidates on its own \~55s
window. Expect a couple of minutes of skew after any edit; a returning visitor can be up to
a day behind.

## Local storage was cleared

Clearing storage discards the anonymous id, and a fresh one is minted on the next visit. That's
a new identity, so a new assignment. Incognito windows start fresh every time — which is why
testing in incognito can show you a different variant each session.

Note this is why AsyncStorage is a required peer for React Native: without persistence the id
is re-minted every launch, re-bucketing every user continuously.

## Someone is using a forced variant

QA overrides pin a variant for testing:

```
https://yourapp.com/?trevo_force=checkout-cta:treatment
```

They persist for the browser tab session. If a teammate is looking at a forced variant, they'll
disagree with everyone else and with their own next session. Clear it with:

```
?trevo_force=clear
```

Forced variants record no exposure, so they don't contaminate results — but they do confuse
side-by-side comparisons.

## Everyone sees control

Different problem — that's usually not a mismatch at all:

* The experiment key in your code doesn't match the one in Trevo. An unrecognised key
  returns `'control'` and logs no warning. Use `defineExperiment()` to get a console warning
  when your declared variants drift from the live config
* The experiment is paused, ended, rejected or stopped by a guardrail, so it left the config.
  Every visitor gets control, including those already bucketed: in the browser within about a minute (the SDK's config
  poll), in a mobile app within five minutes or when it is next opened. Those control reads
  record no exposure. A returning visitor's first page uses the config their browser cached
  until the first fetch lands: a cache from before the stop shows the stopped variant and
  records one exposure, and a cache from before a resume shows control (unless the page
  passed a server-resolved `bootstrap`, which holds until the fetch)
* The experiment is running but its linked PRs haven't merged — Trevo holds 100% on control
  until they do
* The experiment shipped more than 30 days ago and aged out of the config. That reverts every
  bucketed visitor to control, including those on the winner, so merge the cleanup PR before day 30
* Config hasn't loaded yet (check `trevo.isReady()`). A page that reads only before `ready()`
  shows control and records no exposure; the console warns and the experiment's page shows a
  `read_before_ready` health warning. See
  [Flicker on first paint](/troubleshooting/flicker-on-first-paint#why-it-happens)
* The SDK never initialised — see [No events arriving](/troubleshooting/no-events-arriving)

## Confirming an assignment

Assignment is reproducible, so you can check what a given user *should* get without guessing.
Given their identity and the experiment key, the same hash runs everywhere — including in your
own tooling. See the [bucketing spec](/reference/bucketing-spec).

For a quick manual check in the browser:

```js theme={null}
Trevo.getVariant('checkout-cta', { trackExposure: false })
```

Reading it this way doesn't record an exposure, so support debugging doesn't pollute results.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.