Skill 104 · Resolving Ingestion Warnings
Subchapter 104.4
references/fixing-cookieless-warnings.mdMarkdown7 KBView on GitHub
A cookieless-mode event was dropped: either the project has not enabled cookieless tracking, or one of the hash inputs for the cookieless distinct ID (timestamp, user agent, IP, host) was missing or invalid. Category , severity for all six types: the event cannot be ingested at all.
eventerrorThe SDK sent the event in cookieless mode (cookieless_mode: "always" or "on_reject" in posthog-js, sentinel distinct ID $posthog_cookieless), but Cookieless tracking is disabled in the project settings (Settings → Web analytics → Cookieless tracking, stored as cookieless_server_hash_mode). Ingestion drops every such event before any identity is computed, so heatmap events and exception captures sent in cookieless mode are dropped the same way. The events still show in Live events, because that view reads the stream before the pipeline runs, which is why this looks like PostHog accepting data and losing it.
The server-side gate is deliberate: without it, anyone holding the public project token could force cookieless processing on a project that never opted in, which strips IP and user agent. So the fix is a settings change, not a code change:
cookieless_mode from the posthog-js config if cookieless was enabled in the SDK by accident.Verify by re-querying system.ingestion_warnings with posthog:execute-sql (filter type = 'cookieless_team_disabled', timestamp after the change) — no new occurrences — and confirm cookieless events appear in Explore with computed anonymous IDs.
The rest of this file covers the five hash-input warnings, which only fire once the setting is on.
In cookieless mode the client stores nothing — events arrive with the sentinel distinct ID $posthog_cookieless, and PostHog computes a rotating anonymous ID server-side by hashing calendar day + user agent + IP + host. Every warning in this family is one missing hash input:
| Type | Missing hash input |
|---|---|
cookieless_missing_timestamp | No usable timestamp (event timestamp, sent_at, or arrival time) |
cookieless_timestamp_out_of_range | Timestamp’s calendar day isn’t plausibly current — in the future, or further past than ingestion lag allows |
cookieless_missing_user_agent | No $raw_user_agent property |
cookieless_missing_ip | No $ip property |
cookieless_missing_host | No $host property |
The daily rotation is the privacy property — and it means cookieless has a built-in constraint: events must arrive close to when they happened. Old events can’t be identified retroactively, by design.
Who supplies which hash input (browser flow): posthog-js sends $raw_user_agent and $host on every event; $ip is never sent by the client — capture records the connection’s client IP and ingestion attaches it as $ip when the property is absent. Stock posthog-js in cookieless mode therefore works with no extra setup.
Which hash input is missing points at which layer is broken:
posthog:execute-sql: SELECT timestamp, details FROM system.ingestion_warnings WHERE type IN ('cookieless_missing_timestamp', 'cookieless_timestamp_out_of_range', 'cookieless_missing_user_agent', 'cookieless_missing_ip', 'cookieless_missing_host') AND timestamp > now() - INTERVAL 7 DAY ORDER BY timestamp DESC LIMIT 20 (narrow to a single type to isolate one hash input). The details JSON carries the event name and UUID.$raw_user_agent / $host missing → the events aren’t coming from stock posthog-js: a non-browser producer sending the cookieless sentinel, or middleware (before_send, a rewriting proxy) stripping properties.$ip missing → the capture path saw no client IP — a proxy/CDN in front of PostHog not passing the client address through, or middleware explicitly deleting $ip before the identity is computed.cookieless_team_disabled warnings, the project setting is off — see the section above. Hash inputs are not checked until it is on.before_send hook), and ensure any proxy in front of PostHog forwards the client IP.$raw_user_agent, $host, and $ip from the original browser request on every cookieless event, and timestamp at capture time. The $ip is mandatory here even though no warning demands it while your server’s IP fills the gap: without it, every user hashes on the server’s IP and collapses into shared identities — silently, with no warning at all.$ip before PostHog, exempt cookieless events — the IP is a hash input, not stored as identity.Re-run the flow, re-query system.ingestion_warnings with posthog:execute-sql (filter type IN ('cookieless_missing_timestamp', 'cookieless_timestamp_out_of_range', 'cookieless_missing_user_agent', 'cookieless_missing_ip', 'cookieless_missing_host'), timestamp after your fix) — no new cookieless_* occurrences — and confirm cookieless events appear with computed anonymous IDs.