In the GA4 BigQuery export, how do user_pseudo_id and user_id differ?
answer
- one is the device, one is the person
- present on every row versus sometimes null
- cookies clear, the identifier changes
- only set from the moment you set it
- cross-device needs the identifier you supply
basics
~20 suser_pseudo_id is a device- and browser-scoped identifier derived from the client id, present on every event and reset when storage is cleared. user_id is the identifier you explicitly set, present only on events collected after you set it.
solid answer
~50 s`user_pseudo_id` comes from the GA4 client id stored in the browser or the app instance id. It identifies a **browser on a device**, not a person: a visitor on phone and laptop is two `user_pseudo_id` values, clearing cookies mints a third, and a private window makes a fourth. It is populated on essentially every exported event. `user_id` is whatever stable identifier you pass yourself — typically your account id — and it appears **only on events collected after you set it**, so pre-login events from the same visit carry a NULL `user_id`. Cross-device stitching in the export therefore depends entirely on `user_id`; the export contains no Google Signals identity data, so the UI's cross-device figures cannot be reproduced from it. The common modelling trick is to backfill a login identity across the session by joining on `user_pseudo_id` plus `ga_session_id`.
code
javascript · 6 lines// set the identity before the events you want it stamped on
gtag('config', 'G-XXXXXXXXXX', {
user_id: 'a1b2c3' // pseudonymous internal key, never an email address
});
gtag('event', 'plan_upgrade_clicked', { to_tier: 'pro' });go deeper
Recall that one identifier is assigned automatically per browser or app install and the other is one you set yourself, and that only the one you set can follow a person across devices.
Explain when each column is populated, why pre-login events have a null user_id, and how a session-level backfill fills the gap. Know that clearing cookies mints a new user_pseudo_id.
Demonstrate the reconciliation judgment: why export-derived user counts never match the interface, what Google Signals and modelled data add on the UI side only, and how you name device versus person metrics so nobody compares them by accident.
Own the identity model across sources: what the canonical person key is, whether an identity graph stitching devices to accounts is worth its false-merge risk on shared devices, and how you communicate a metric that drops because login coverage improved.
## Two identifiers with different guarantees Every row of the GA4 BigQuery export has a `user_pseudo_id` column and a `user_id` column. They answer different questions and confusing them produces user counts that are wrong in a direction nobody notices. **`user_pseudo_id`** is derived from the GA4 client id — for web, a value the tag stores in a first-party cookie; for apps, the app instance id. Its scope is *this browser on this device*, or *this installation of the app*. GA4 populates it automatically, so it is present on nearly every event, which makes it tempting as "the user column". It is not one: - The same person on a phone and a laptop is two `user_pseudo_id` values. - Clearing site data, using a private window, or switching browsers mints a new one. - Reinstalling an app produces a new app instance id. - Browser storage policies age cookies out, so long-gap returning visitors can appear as new ones. The net effect is that `COUNT(DISTINCT user_pseudo_id)` counts **devices/browsers**, and it drifts upward relative to real people over time. **`user_id`** is a value *you* supply — on web via the GA4 configuration (for example `gtag('config', 'G-XXXXXXXXXX', { user_id: 'a1b2c3' })`, or by setting it before sending events), and via the equivalent SDK call in apps. Its scope is whatever you make it, normally your own account identifier, and it is the only mechanism that ties activity across devices together in the exported data. Because GA4's terms prohibit sending personally identifying data, the value must be a pseudonymous internal key — not an email address, not a raw username. ## The timing problem `user_id` is stamped onto events only from the moment it is set. In a typical visit the sequence is: land on the marketing page, browse, then log in. Every event before the login carries `user_id = NULL`, and only the post-login events carry it. So a naive `COUNT(DISTINCT user_id)` over an event window silently answers "how many people did something *after* logging in", not "how many people visited", and any funnel keyed on `user_id` loses its top steps. The standard fix is to backfill identity within a session: group the session by `user_pseudo_id` plus `ga_session_id`, take a non-null `user_id` from anywhere in that session, and apply it to the session's earlier events. A stronger version resolves an identity graph — every `user_pseudo_id` that was ever seen with a given `user_id` belongs to that person — but that only holds if you accept the risk of shared devices, and it is a modelling decision to make deliberately rather than by accident. ## What the export deliberately does not contain The GA4 interface can report cross-device behaviour using **Google Signals** — identity contributed by signed-in Google accounts that have ads personalisation on. That data is not exported to BigQuery. Neither are the modelled conversions and modelled users that GA4 reports when consent mode indicates a visitor declined cookies: the export contains observed events only. Consequently a user count computed from the export will not reconcile with the same figure in the UI, and the difference is not a bug in your SQL. ## Reporting identity settings GA4 properties have a reporting identity setting that determines the precedence the interface uses — user id first, then signals, then device, with a modelled component. That setting affects the UI. It does not change the export, which always ships `user_pseudo_id` and `user_id` as collected. If someone asks why the UI's active-user figure moved without a code change, a change to that setting is a candidate; the exported rows will be unchanged. ## Practical guidance Model both. Keep `user_pseudo_id` as the device grain, derive a resolved person key from `user_id` where available, and be explicit in every metric about which grain it counts. Publish device counts and person counts under different names — "browsers" and "identified users" — so nobody quietly compares one to the other across a release where login coverage changed. And remember that raising login coverage will *reduce* your apparent user count while increasing data quality, which is a conversation to have with stakeholders before it happens, not after.
- Why does COUNT(DISTINCT user_pseudo_id) drift above the real number of people over time?Because it counts browsers and app installations, and those multiply. One person accumulates values across phone, laptop, a private window, a cleared cookie jar and a reinstall, while browser storage policies age older values out and turn returning visitors into apparent new ones. It is a usable device metric and a misleading person metric.
- Can you reproduce the GA4 interface's cross-device user numbers from the BigQuery export?No. The interface can use Google Signals identity from signed-in Google accounts and adds modelled users where consent was declined; neither is exported. The export carries observed events with user_pseudo_id and whatever user_id you set. Expect a permanent gap and explain it up front rather than trying to make the two numbers agree.
- What must you never put in the user_id value?Anything that identifies a person directly — email address, phone number, name, or a raw username. GA4's terms prohibit sending personally identifying data, and violations can cost the property's data. Use a pseudonymous internal key that only your systems can resolve, and keep the mapping table on your side of the fence.
saying these in an interview costs you the question
- Calls user_pseudo_id the user and counts people with it
- Assumes user_id is present on every event in a session
- Expects the export to reproduce the UI's cross-device users
- Puts an email address in the user_id value
- Thinks the reporting identity setting changes the exported rows