Why do my GA4 custom dimensions/parameters show as (not set) or never appear in reports?
Quick answer
GA4 does not automatically turn every event parameter into a report-ready dimension - it has to be explicitly registered under GA4 Admin > Custom definitions, and registration only applies going forward from the moment it's created, never retroactively to already-collected events. (not set) specifically means the dimension exists in the report but the parameter was absent, empty, or unregistered on that particular hit.
Why This Happens
GA4 stores every parameter you send in raw event data (visible in BigQuery export if linked), but the UI's reports only surface parameters you've explicitly registered as a custom dimension or metric, up to your property's limit (50 event-scoped custom dimensions on most GA4 properties). An unregistered parameter is being collected correctly and is simply invisible in the standard interface.
Registration is also not retroactive: if you register a custom dimension today, historical events from before that moment will show (not set) for that dimension forever, because GA4 doesn't reprocess old data to backfill a definition that didn't exist yet. This is the single most common reason a dimension 'suddenly' has months of (not set) history even though the parameter itself was sent correctly.
The other frequent cause is scope mismatch: an event-scoped parameter used in a report built around user-scoped dimensions (or vice versa) can show as (not set) even though the underlying data is fine, because GA4 can't join the two scopes the way the report is asking it to.
Fix It
Confirm the parameter is actually registered
Go to GA4 Admin > Custom definitions > Custom dimensions and search for the exact parameter name. If it isn't listed, click Create custom dimension, set the correct scope (Event, User, or Item), and map it to the exact parameter name your tag sends - the mapping is case-sensitive and must match exactly.
Check the registration date against your reporting window
Custom definitions have a creation timestamp. If your report's date range starts before that timestamp, expect (not set) for all rows before it - this isn't a bug to fix, it's a limitation to account for when reading the report.
Match scope to how you're using it
If you need to see the value at the user level (e.g. a plan tier that doesn't change within a session), register it as a User-scoped dimension via a user property, not just an event parameter. Event-scoped dimensions only apply to the specific event they were sent with.
Verify the parameter is actually present on the hit, not just intended to be
Use DebugView to inspect a live event's parameter list. If the parameter itself is missing, empty string, or misspelled at the tag/dataLayer level, no amount of correct registration in GA4 will fix it - the collection side needs the actual fix.
How To Verify It Worked
In DebugView, click the event in question and expand its parameters - confirm the exact parameter name and a non-empty value are present on the live hit before checking the report side at all.
Then, in a standard report or Exploration, add the custom dimension as a row/column and set the date range to start after the dimension's registration date. A pass looks like real values appearing for events sent after registration; (not set) persisting only for the pre-registration date range is expected and not a bug.
Still Not Fixed?
We audit and fix GA4 implementations for a living - if this doesn't resolve it, the next step is usually a full event-by-event audit.
Talk to a GA4 Consultant