GA4 Custom Dimensions: The Practitioner’s Guide

ga4 custom dimensions

Most people treat GA4 custom dimensions as a checkbox — send a parameter, register it in Admin, done. Then two weeks later they open a report and half the rows say “(not set)”, the historical data they wanted is missing, and the dimension they built for tracking individual URLs has collapsed into a single “(other)” bucket. The mechanics aren’t hard, but they punish the wrong mental model. Custom dimensions are not a database field you can query retroactively; they are a forward-only recording instruction with hard limits, a strict naming contract, and a cardinality ceiling. Get the model right and they become the most useful reporting layer in the product. Get it wrong and you burn a quota slot on a dimension that never populates.

What a custom dimension actually is

A GA4 custom dimension is a mapping between an event parameter you send with your hits and a reporting field you can slice by. GA4 collects hundreds of parameters it never surfaces on its own — anything you attach to an event beyond the automatically-collected set sits in the raw stream but is invisible in reports until you register it. Registration is the act of telling GA4: “promote this parameter into a dimension I can use as a row or a filter.” Two things must both be true for a custom dimension to work: the parameter is being sent on your events, and the parameter is registered in Admin under the exact same name. Miss either half and you get nothing, or worse, silent partial data.

Scope: the decision that’s hard to reverse

Every custom dimension has a scope — event, user, or item — and choosing wrong means deleting it and starting over, because scope can’t be edited after creation. The common advice (“ask if the value could differ between two events in the same session”) is a decent first filter but too coarse. Use this three-question test instead:

  • Does the value describe a single interaction? Then it’s event-scoped — content type, form name, search term, scroll milestone. This is where 80% of your dimensions live.
  • Is it a durable property of the person that should attach to every future event? Then it’s user-scoped — plan tier, logged-in state, customer vs prospect. User-scoped dimensions take the last value seen and apply it going forward; they don’t rewrite history.
  • Does it describe a product inside an e-commerce items array? Then it’s item-scoped — product brand, category, or a custom SKU attribute.

The trap is user scope. People reach for it thinking “I want this to persist,” but user-scoped dimensions can’t be used in most standard reports and behave unintuitively in explorations, because GA4 resolves them to the most recent value per user. If you only ever need the value at the moment of an event, event scope is the safer choice.

Registration is forward-only — plan before you need it

This is the single most expensive lesson in GA4. Registering a custom dimension does not backfill. GA4 populates the dimension from the moment of registration forward, and every event before that timestamp shows “(not set)” for that field, permanently — there is no reprocessing button. Launch a campaign on Monday, realize on Friday you wanted to segment it by a custom parameter, and the Monday-through-Thursday data is gone as a dimension, even though the parameter values sit in the raw event stream.

The practical rule: register the dimension the same day you start sending the parameter, ideally before. Keep a lightweight measurement plan — a spreadsheet of parameter name, scope, and reason — so you’re not burning slots on junk dimensions you’ll delete later.

There is one real escape hatch, and the tidy help-doc version leaves it out: the parameter values still exist even when the dimension shows “(not set)”. If you have the free BigQuery export enabled, every parameter you ever sent — registered or not — lands in the raw event tables. You can reconstruct a “custom dimension” for a historical period by querying event_params in SQL and unnesting the key you want, long before you formally registered it in the GA4 UI. So the durable pattern is: enable BigQuery export on day one, send rich parameters even before you’ve decided how to report on them, and register dimensions in the UI only for the ones you slice regularly. UI registration is your convenient reporting layer; BigQuery is your source of truth and your retroactivity insurance.

The limits you’ll actually hit

GA4 standard properties cap you at 50 event-scoped custom dimensions, 25 user-scoped, and around 25 item-scoped, plus 50 custom metrics. Those numbers sound generous until a tag-management sprawl fills them with half-used experiments. Two rules keep you under the ceiling: archive dimensions you’ve stopped using (archiving frees the slot but keeps historical data intact and lets you restore later), and never register a parameter “just in case” — that’s what the raw stream and BigQuery are for. GA4 360 raises the ceilings, but almost no one hitting the limit on a standard property has a real need for 50 live dimensions; they have a cleanup problem.

Setting one up end to end

The full loop has five steps, and the failure modes cluster around steps 3 and 4:

  • Send the parameter on the relevant event, via gtag or Google Tag Manager.
  • Verify it in DebugView before you touch Admin — confirm the parameter name and a real value are actually arriving.
  • Register it under Admin → Custom definitions → Create custom dimension, mapping it to the parameter.
  • Match the name exactly. The event parameter name and the registered parameter name must be identical, and GA4 is case-sensitive: content_type and Content_Type are two different fields. This mismatch is the number-one cause of a dimension that never populates.
  • Wait 24–48 hours for standard reports to build. DebugView and Realtime show data almost immediately; the aggregated tables lag.

GTM vs gtag: where the naming contract breaks

The exact-match rule bites hardest when Google Tag Manager sits in the middle. In GTM, the parameter name that reaches GA4 is whatever you type into the tag’s event-parameter field — not your dataLayer variable name. So a dataLayer variable called articleCategory pushed into a GA4 event tag with the parameter key content_type registers as content_type. Beginners routinely register the dataLayer name, or the variable name, or a camelCase version, and then wonder why the dimension is empty. Standardize on snake_case keys, keep one naming convention across your container, and always read the value straight off DebugView — the name there is the literal string you must register, full stop.

Why your dimension shows “(not set)”

“(not set)” is GA4 telling you it recorded an event but had no value for this dimension on it. There are five distinct causes, and diagnosing which one you have saves hours:

  • The parameter wasn’t sent on that specific event (e.g., you fire content_type on page_view but the event in the report is scroll).
  • The event predates registration — forward-only behavior, unfixable in the UI.
  • A name mismatch — case or spelling drift between sent and registered.
  • Tag-firing order — the parameter’s variable resolves after the tag fires, so an empty value goes out.
  • Data thresholding — when signals like Google Signals are on and volumes are low, GA4 withholds rows to protect identity, surfacing them as “(not set)” or “(other)”.

Cardinality and the “(other)” collapse

GA4 tolerates a limited number of unique values per dimension per day before it starts folding the excess into an “(other)” row. High-cardinality dimensions — anything approaching a unique value per event, like a full URL with query strings, a user ID, or a timestamp — will blow past that ceiling and render most of your data useless as “(other)”. The fix is to design for buckets, not identifiers. Instead of the raw publish date, bucket it into “0–30 days / 31–90 / 91–365 / 1yr+”. Instead of a full URL, capture a content-type or template label. If you truly need per-URL granularity, that’s a BigQuery job, not a custom dimension.

A worked micro-example: content_type for a blog

Say you run a content site and want to compare organic engagement across article types. You add a single event parameter, content_type, to your page_view event, populated from a dataLayer value your CMS already knows: article, guide, comparison, or landing. Four clean buckets — low cardinality by design. You verify in DebugView that content_type: guide is arriving, register an event-scoped custom dimension named exactly content_type, and wait a day. Now in an exploration you put content_type as the row, engagement rate and conversions as columns, and segment by organic search. Within a week you can see, with real data, that your comparison pages convert at double the rate of your general guides — a signal that should reshape your content roadmap. That’s a custom dimension earning its slot.

A dimension only pays off if the finding changes what you publish next. That “comparison pages convert 2x” signal is a prompt to find every comparison keyword you’re not yet ranking for — which is where an SEO workflow closes the loop. In SEO Rocket, that same content-type signal maps onto keyword research run on real Ahrefs data and a competitor gap analysis that surfaces the exact comparison terms your rivals rank for and you don’t. You point the validation-gated AI writer at the highest-value gaps, publish, and track movement — a playbook proven across 1,000,000+ ranking pages. GA4 tells you which content type performs; the SEO tooling tells you where to make more of it.

A starter set worth creating for SEO work

If you’re instrumenting a content site from scratch, these four event-scoped dimensions earn their slots immediately:

  • content_type — article, guide, comparison, landing. The single most useful cut for content performance.
  • content_age_bucket — bucketed publish date, so you can see whether fresh or evergreen content drives engagement.
  • author — content owner or team, for attribution across a multi-writer operation.
  • logged_in — a simple yes/no that separates audience from customers in nearly every report.

Keep every value low-cardinality, send it on page_view, and register it the day you ship it. That’s a clean foundation you won’t have to rebuild.

Frequently asked questions

Do GA4 custom dimensions apply to historical data?

No. Registration is forward-only. A newly registered dimension shows “(not set)” for every event before you created it, with no way to backfill inside the GA4 interface. The only way to recover historical values is to query the raw parameters in the free BigQuery export — which is why enabling that export early is the standard insurance against late registration.

What’s the difference between a custom dimension and a custom metric?

A custom dimension holds a text value you slice by (content type, plan tier). A custom metric holds a numeric value you sum or average (a custom score, a price, a count). If you’d put it on a chart’s x-axis or a table row, it’s a dimension; if you’d add it up, it’s a metric.

How many custom dimensions can I create in GA4?

Standard properties allow 50 event-scoped, 25 user-scoped, and roughly 25 item-scoped custom dimensions, plus 50 custom metrics. Archiving an unused dimension frees its slot while preserving the historical data, so clean-up is almost always a better answer than requesting more than the ceiling.

Why does my custom dimension show “(other)” instead of real values?

You’ve exceeded GA4’s daily cardinality limit for that dimension — too many unique values, usually because you captured an identifier (full URL, user ID, raw timestamp) instead of a bucket. Redesign the parameter to emit a small set of grouped values, and route true per-item granularity to BigQuery.

The bottom line

GA4 custom dimensions reward one habit above all others: decide what you’ll measure before you need the data, because registration never looks backward. Pick scope deliberately, register on the same day you send the parameter, match names character-for-character between your tag and Admin, and design every value as a bucket rather than an identifier so cardinality never eats your reports. Enable BigQuery export as your retroactivity backstop, and treat each dimension as one half of a loop that ends in a decision — usually a decision about what to build or publish next. GA4 is your measurement layer; pairing it with the rank tracking and site audit inside a tool like SEO Rocket closes the gap between “which content performs” and “where to make more of it.” Instrument with that discipline and custom dimensions stop being a source of “(not set)” frustration and start being the clearest window you have into how your site actually performs.

Questions? Chat with us