GA4

Custom Dimensions vs Event Parameters: The Registration Gap

Published 

A laptop on a desk showing a reporting dashboard of graphs and metrics

Here is a failure mode that costs teams months of data and almost never gets caught quickly.

A developer adds checkout_step to the dataLayer. The GTM tag picks it up and sends it. You open the network tab and there it is: ep.checkout_step=2, on every hit, exactly as specified. The implementation is correct and everyone signs off.

Three months later, someone tries to build a funnel report on checkout_step and cannot find it. Not "the data looks wrong" — the dimension does not exist. It was never registered. GA4 collected the value on every hit for ninety days, stored it, and made it reachable by exactly nothing.

That is the registration gap, and it is the single most expensive silent failure in GA4.

The two-step model

GA4 separates collecting a value from making it reportable. These are independent, and neither one tells you about the other.

Step one: the event parameter. Your tag sends checkout_step=2 alongside the event. This is what appears in the payload as ep.checkout_step (string) or epn.checkout_step (numeric). Reading the raw hit shows you this is working — see reading a GA4 collect payload for the parameter prefixes.

Step two: the custom definition. In Admin → Custom definitions, you register checkout_step as a custom dimension. This creates the reporting field. Only now can a report, exploration, audience, or segment reference it.

Universal Analytics forced these together — you sent data to dimension3, and dimension3 was a slot that already existed. GA4's parameters are free-form strings, which is far more flexible and removes the guardrail entirely. Nothing warns you that you are sending a parameter nobody registered.

Registration is not retroactive, with one important caveat. When you register a custom dimension, GA4 backfills it — but only for the standard data-retention window on data that is still retained. If your property is on the 2-month default and the parameter has been collecting for six months, four of those months are simply gone. This is a good reason to check your retention setting at the same time.

The reverse failure

The gap runs in both directions, and the second direction is more visible but less understood.

A dimension is registered and nothing populates it. Someone creates customer_tier in Custom definitions, expecting the implementation to follow. It never does, or it ships sending customerTier instead. The dimension exists in every report picker, so people build segments and explorations on it, and every single one returns (not set).

This is worse than the first failure in one specific way: the first failure is invisible, but the second one actively produces reports that look real. A funnel built on an unpopulated dimension does not error — it shows zero, and zero looks like a business finding.

Scope: event, user, or item

Every custom definition has a scope, and it is fixed at creation. Getting it wrong means deleting and recreating, which burns one of your limited slots.

Event-scoped applies to the single event it arrived on. checkout_step, search_term, video_title, form_name. This is the default and the right answer most of the time.

User-scoped attaches to the user and persists across sessions and events until overwritten. customer_tier, account_type, lifecycle_stage. Sent as up.<name> in the payload. Use this when the attribute describes the person, not the interaction. The trap is that it applies retroactively to nothing and forward to everything — a user's tier in reports is whatever you last sent, applied across the whole reporting window.

Item-scoped applies inside the items array on ecommerce events. item_colour, item_supplier, item_condition. Available only on properties with ecommerce data. See the GA4 ecommerce dataLayer spec for how items are structured.

Choosing user-scope for something event-scoped is the common error. search_term as a user property means every report shows only that user's most recent search, applied to all of their history.

Dimension or metric?

A parameter with a numeric value can be registered as either. The distinction is whether you want to group by it or aggregate it.

Custom dimension — you want to break reports down by this value. checkout_step, plan_name, error_code. Even numeric values are often dimensions: checkout_step as a metric would give you the meaningless sum of all step numbers.

Custom metric — you want to sum or average it. shipping_cost, time_to_first_byte, items_in_basket. Registered with a unit (standard, currency, distance, time) that GA4 uses for formatting.

Register it as a metric and it must arrive as epn. in the payload, not ep.. A numeric value sent as a string will not aggregate. This is a real and common mismatch: value: "49.99" in the dataLayer, quoted, becomes ep.value and never sums.

The limits, and how fast you hit them

Standard properties get:

  • 50 event-scoped custom dimensions
  • 25 user-scoped custom dimensions
  • 50 custom metrics
  • 10 item-scoped dimensions

Fifty sounds generous until you actually plan an implementation. A mid-sized ecommerce site with a content section, a search feature, form tracking, and a login area will use thirty without trying. An agency running a shared property across brands will run out.

Three habits help:

Register nothing speculatively. A dimension registered "in case we need it" that nothing populates costs a permanent slot and produces (not set) in every report.

Reuse generic names. form_name used across every form beats newsletter_form_name, contact_form_name, and demo_form_name as three separate dimensions.

Archive, do not accumulate. GA4 lets you archive a custom definition, which frees the slot. Archiving does not delete the historical data — it stops the dimension being reportable going forward. Sweep quarterly.

Once you are genuinely at the limit, the BigQuery export is the answer: every parameter is in the raw data whether or not it was ever registered, so the ceiling only applies to the GA4 interface.

Finding the gap in your own property

The check is a set comparison in two directions:

  1. Every parameter your tags send → is it registered?
  2. Every registered dimension → is anything sending it?

Doing this by hand means opening every GA4 event tag in GTM, listing its event parameters, listing the parameters on your gtag calls if any are hard-coded, then diffing that against Admin → Custom definitions. On a container with forty tags this is a solid afternoon, and it is exactly the kind of task that gets deferred until someone needs a report that does not exist.

Two shortcuts that do not require the full comparison:

Check the DebugView parameter list. Fire an event and look at the parameters GA4 received. Anything listed there that you cannot find in Custom definitions is unregistered.

Look for (not set) in reports. A dimension that is overwhelmingly (not set) is almost always the reverse failure — registered, unpopulated. Worth noting that (not set) has other causes too; data quality triage covers distinguishing them.

A worked example

An ecommerce team wants to report on which internal search terms lead to purchases.

What they build. The dataLayer pushes search_term on the results page and search_term again on add_to_cart if the item came from search. The GTM tag sends it. The payload shows ep.search_term=running%20shoes. Implementation complete.

What is missing. search_term is never registered as an event-scoped custom dimension.

What happens. For four months, every search is collected and none is reportable. The team builds their exploration, cannot find the dimension, and files a bug against the implementation — which is correct. The developer re-verifies the payload, which is also correct. Two weeks of back-and-forth before someone opens Custom definitions.

The fix. Admin → Custom definitions → Create custom dimension. Name it Search Term, scope Event, parameter search_term. Reporting starts working, backfilled only as far as the retention window allows.

The cost. However much of those four months fell outside retention. If the property was on the 2-month default, two months of search-to-purchase data that will never exist.

How Tagfire helps

The comparison this article describes is exactly what the GA4 Sync Checker automates. Connect a GTM container and a GA4 property, and it reads both sides:

  • Parameters sent but not registered — the invisible-data failure, listed by parameter name and by which tags send it.
  • Dimensions registered but not populated — the (not set) failure, listed by dimension and scope.
  • A sync score so you can track the gap closing across an engagement rather than re-deriving the list every time.

The GA4 Audit covers the surrounding configuration in the same pass — data retention (which determines how much backfill you actually get), custom-definition slot usage against the limits, and key-event setup. Run it alongside the sync check and you have both the gap and the context for how expensive it is.

If you are still at the implementation stage, the Tag Debugger confirms what is actually on the wire before you go registering anything, and the Payload Decoder breaks a single hit down parameter by parameter with no account needed.

The documentation covers running a sync check and reading the results.

Related reading
An analytics dashboard of charts and trend lines filling a laptop screen
GA4

How to Audit a GA4 Property in 10 Minutes

A repeatable GA4 audit process: what to check, in what order, and how to tell a real problem from a cosmetic one. Includes the six findings that appear in almost every account.

Read article
Hand-drawn wireframes of three page layouts sketched side by side
GA4

The 2026 GA4 + GTM Setup Checklist

A complete technical checklist for Google Analytics 4 and Google Tag Manager — collection, configuration, consent, campaign tracking, and governance. Verify each item before you trust a single report.

Read article