# Real Time Identity Log Tables

Real-time ID stitching decides which incoming IDs belong to which profile. The identity log tables record each of those decisions as a queryable row, so when a profile looks wrong you can find the reason in SQL: which key failed validation, which IDs pulled a merge together, and which IDs a profile lost at its association limit.

Two tables carry this record. The `id_changes` table logs every change to the identity graph, including evictions at the 200-association limit. The `validation_failures` table logs key/value pairs on incoming events that failed validation and were therefore excluded from stitching. Both are written through the standard ingest pipeline, so rows are typically queryable within a minute of the event that produced them.

Use this page as a reference for the columns in each table and the queries that answer the most common questions. For the Datadog dashboards covering throughput and errors, see [Real Time Observability and Logs](/products/customer-data-platform/real-time/real-time-observability-and-logs). For the `activations` table, see [Reviewing Real Time Triggered Activation Logs](/products/customer-data-platform/real-time/reviewing-real-time-triggered-activation-logs).

## What Gets Logged, and When

The `id_changes` and `validation_failures` tables record identity changes and exceptions, not a running trace of healthy traffic. Knowing which table receives a row — and when nothing is written at all — is what makes them diagnostic.

| Situation  | Table  | What It Tells You  |
|  --- | --- | --- |
| An event creates, merges, or removes a profile | `id_changes` | The identity-graph history: which profile changed and how |
| An event stitches to a profile holding 200 associations | `id_changes` | Which IDs were evicted to make room |
| A key/value pair on an incoming event fails validation | `validation_failures` | Which pairs were rejected, and the reason for each |
| Configuration is correct and no limit is reached | Neither | Nothing is written — a quiet `validation_failures` table means your keys are valid |


A healthy, stable configuration produces no validation failures and no evictions. Rows accumulating in `validation_failures` signal that something upstream — a tracking implementation, a bulk upload, a key name — needs fixing. They are not a normal operating condition.

Identity logging on the event-processing path is enabled for every instance and requires no opt-in. Logging on the personalization request path is handled separately; see [Personalization Path Logging](#personalization-path-logging).

## Live Traffic Is Never Blocked

Real-time key validation never rejects a live request. An event carrying an invalid key is still processed, because refusing it would slow or break the page the visitor is on. The invalid pair is excluded from stitching, the rest of the event proceeds, and a row is written to `validation_failures`.

## The id_changes Table

Each row in `id_changes` records one change to the identity graph. The `profile_change_type` column identifies which kind of change it was, and determines which of the other columns are populated.

| Column  | Type  | Description  |
|  --- | --- | --- |
| `time` | int | When the row was emitted, as Unix epoch seconds. Filter it with `TD_INTERVAL` or `TD_TIME_RANGE`. |
| `event_type` | string | Always `id` for rows in this table. |
| `reactor_instance_id` | string | The real-time instance that emitted the row. |
| `profile_change_type` | string | The kind of change. See [Profile Change Types](#profile-change-types). |
| `current_rid` | string | The profile the change happened on, as a UUID. |
| `current_id_attributes` | `array<string>` | The IDs carried by the triggering event that were not already on the profile, as `keyname:value` strings. |
| `key_values` | string | The profile's full association set after the change, comma-separated. `NULL` on `profile_ids_evicted` rows. |
| `merged_rids` | `array<string>` | The profiles absorbed into `current_rid` by the merge. Populated on `profile_updated_by_stitching`, `NULL` otherwise. |
| `evicted_ids` | string | A JSON array of the IDs removed to stay within the 200-association limit. Populated on `profile_ids_evicted`, `NULL` on every other row. |
| `td_rt_tracking_id` | string | The tracking ID from the originating event, when the event carried one. Otherwise null. |


### Profile Change Types

| Value  | Meaning  |
|  --- | --- |
| `profile_added` | The event matched no existing profile, so a new one was created. |
| `profile_updated_by_stitching` | The event matched an existing profile, which absorbed the incoming IDs. `merged_rids` lists the profiles folded in. |
| `profile_deleted_by_stitching` | The profile was removed because a merge folded it into another profile. |
| `profile_ids_evicted` | Adding the incoming IDs would have exceeded 200 associations, so IDs were evicted. `evicted_ids` lists them. |


Exactly one of `profile_added`, `profile_updated_by_stitching`, or `profile_deleted_by_stitching` is written per triggering event, since a profile can only be created, updated, or deleted at one time. A `profile_ids_evicted` row is written *in addition* to the `profile_updated_by_stitching` row for the same event, and both carry the same `current_id_attributes` — that shared value is how you pair them.

Reading a Merge
`current_id_attributes` holds only the IDs the triggering event contributed that were new to the profile — not every ID that ended up on it. That distinction is what lets you tell *why* a merge happened, rather than only *that* it happened. `key_values` holds the full set afterwards.

### The 200-Association Limit

A profile stores at most 200 associations, counting both merged profiles and IDs. When an incoming event would push a profile past that limit, the lowest-priority associations are evicted to make room and a `profile_ids_evicted` row records what was removed.

Eviction can discard IDs the profile already held *or* IDs the incoming event brought — an incoming ID is not guaranteed to win a slot. Both kinds appear together in `evicted_ids`.

Repeated evictions on the same `current_rid` are the strongest available evidence of over-stitching, where an ID graph rule stitches on a value shared across users and collapses unrelated people into one profile. A profile churning at the limit is not storing a coherent identity, so treat these rows as a signal to review the rule that produced them.

## The validation_failures Table

Each row in `validation_failures` covers one incoming event. If that event carried several invalid pairs, all of them appear in a single row rather than one row per pair.

| Column  | Type  | Description  |
|  --- | --- | --- |
| `time` | int | When the row was emitted, as Unix epoch seconds. Filter it with `TD_INTERVAL` or `TD_TIME_RANGE`. |
| `event_type` | string | Always `validation` for rows in this table. |
| `reactor_instance_id` | string | The real-time instance that emitted the row. |
| `invalid_keys` | `array<string>` | One element per rejected pair. Each element is a JSON object with `key`, `value`, and `reason` — for example `{"value":"12345","key":"customer_id","reason":"Value is not a string."}`. Unnest the array, then extract the fields with `json_extract_scalar`. |
| `td_rt_tracking_id` | string | The tracking ID from the originating event, when the event carried one. Otherwise null. |


### Validation Failure Reasons

The `reason` field on each entry in `invalid_keys` holds one of the following messages.

| Reason  | What To Fix  |
|  --- | --- |
| `Key is not present in the config file.` | The event sent a key name your real-time configuration does not define. Add the key to the configuration, or stop sending it. |
| `Key name case does not match a configured key.` | The key name matches a configured key except for capitalization — for example `User_ID` against a configured `user_id`. Correct the case in your tracking code. |
| `Value is not a string.` | The value arrived as a number, boolean, or object. Send identity values as strings; a numeric ID must be quoted. |
| `Value is empty.` | The value was empty, null, or absent. Usually a tracking script reading an attribute before it is populated. |
| `Value does not meet the valid_regexp for this key.` | The value failed the `valid_regexp` defined on that key. Either the data is malformed or the pattern is too strict. |
| `Value is included in invalid_texts for this key.` | The value matched an entry in `invalid_texts` for that key — typically a placeholder such as `null` or `undefined` sent as a literal string. |


## Where the Tables Live

The `id_changes` and `validation_failures` tables are in your parent segment's real-time database, `cdp_audience_<parent_segment_id>_rt`, alongside the `activations` table. Query them from **Data Workbench** > **Queries** > **New Query**, using the Trino engine.

When an event carries a `td_rt_tracking_id`, that value appears on every row the event produced, so one value follows an event across its activation, identity, and validation outcomes. See [Real Time Observability Event Tracking](/products/customer-data-platform/real-time/real-time-observability-event-tracking) for how to attach it.

For ready-made diagnostic queries against these tables, see [AI Skills for Real-Time Debugging](/products/customer-data-platform/real-time/real-time-ai-debugging-skills).

## Managing Log Volume and Retention

The `id_changes` and `validation_failures` tables hold ingested rows that persist indefinitely by default. Because `id_changes` records every identity-graph change, a large bulk ID graph upload writes rows in proportion to the profiles it touches, and a misconfigured tracking implementation can fill `validation_failures` at the rate it sends events.

Two things keep this in check:

- **Fix the underlying problem.** Validation failures and evictions both represent outcomes you do not want. Once the misconfiguration is corrected, the rows stop being written.
- **Set an expiration period on the table.** Configure per-table expiry so rows older than your investigation window are purged automatically. See [Expiring Data From a Table](/products/customer-data-platform/data-workbench/databases/expiring-data-from-a-table).


Expiry Is Not Configured For You
Neither table has a retention period by default. Decide how far back your investigations need to reach before setting one — a short window suits validation failures, while identity-graph history in `id_changes` is often worth keeping longer.

## Personalization Path Logging

Identity logging on the personalization request path is controlled separately from the event-processing path, and is off by default. Emitting these rows during a personalization request adds work to a latency-sensitive path, so it is enabled per instance rather than universally. Contact Customer Support if you need it turned on for an investigation.

## Next Steps

- [Real-Time ID Stitching Overview](/products/customer-data-platform/real-time/real-time-id-stitching-overview) — how stitching decides which IDs belong together.
- [AI Skills for Real-Time Debugging](/products/customer-data-platform/real-time/real-time-ai-debugging-skills) — skills that generate diagnostic queries against these tables for you.
- [Real Time Observability and Logs](/products/customer-data-platform/real-time/real-time-observability-and-logs) — Datadog dashboards for throughput, latency, and errors.