First: is anything arriving at all?
Open Settings and check the write key’s last used time. It updates on every authenticated request:- “Never” (or stale) — requests aren’t reaching Glyph. Keep reading from the environment mismatch down through flushing.
- Recent — requests arrive but the data isn’t where you’re looking, or messages are being rejected. Jump to wiring up
onErrorand the sections after it.
You’re looking at the wrong environment
glyph_test_... keys send to the isolated test environment; glyph_pk_... keys send to live. The environments never mix. Make sure the workspace view matches the key used by the application.
The process exited before flushing
Messages queue locally and flush in batches (default: 20 messages or every 5 seconds). A process that exits with messages still queued loses them.- Serverless functions and API routes — the runtime can freeze or terminate the moment a response is sent.
await glyph.flush()before responding, every time. - Scripts and CLIs — call
close()(which flushes) before exiting. In Node,flushOnExit: trueadds a best-effort safety net. - Python specifically — the client has no background thread. It only flushes when you enqueue a message (and a threshold has been reached) or when you call
flush()/close(). A Python process that goes quiet holds its queue forever. Use the context manager (with Glyph(...) as glyph:) soclose()always runs. - Browser — the SDK makes a best-effort flush when the tab is hidden. The queue is in memory, so a hard navigation, tab close, crash, blocked request, or failed unload can still lose messages. Capture critical events server-side. If you
close()the client or disableautoFlush, you own flushing yourself.
You’re not seeing delivery errors
The SDKs report delivery problems throughonError; they do not throw them from track, profile, group, or page. Configure the callback before debugging delivery:
status tells you which of the next sections applies. Non-retryable failures drop the batch after calling onError — the callback is your only record of what was lost.
401 — the key is wrong, rotated, or revoked
A 401 is non-retryable, so the batch is dropped. Check whether the key was rotated while a deployment still has the old value. Update the environment variable and redeploy. Also check for whitespace or truncation; keys must begin with glyph_pk_ or glyph_test_.
422 — the payload failed validation
The SDKs validate before sending, so 422 mostly affects HTTP API callers: unknown fields (each message type accepts only its documented keys), an empty required string, or an unparseable timestamp. The response body’s issues array names each failing field.
When using an SDK, the equivalent failure is a TypeError thrown at the call site — an empty userId, a missing email trait on profile, a value over 256 characters. Those never reach the queue at all.
403 — the test environment is full
Once the test environment contains 5,000 stored events, further requests return 403. A final accepted batch can place the stored total slightly over the threshold. Clear test data from Settings or switch to a live key. See Write keys.
429 — you’re being rate limited
Each write key has a budget (default 600 messages per minute, sliding window). The SDKs handle occasional 429s themselves — they honor Retry-After and keep the batch queued — so intermittent limiting delays data rather than losing it. Sustained limiting usually means an import is running unthrottled; pace it as shown in the backfill guide.
You’ve hit your plan’s customer profile limit
When the workspace reaches its profile limit, messages for new customers are skipped even though the request succeeds. Messages for existing customers are still accepted. Upgrade the plan or remove unnecessary profiles from the data you intend to backfill.Events are dated January 1970
You passed epoch seconds where epoch milliseconds are expected.1750000000 is June 2025 in seconds but January 21, 1970 in milliseconds — and it parses “successfully”. Multiply by 1,000, or pass an ISO 8601 string / native date object instead.
Two related timestamp pitfalls:
- Python naive datetimes are interpreted as the machine’s local time when serialized. Always construct backfill timestamps with
tzinfo=timezone.utc(or the source’s real zone). - Backfilled events sort by their timestamp, not arrival time — if an import “isn’t showing”, check whether it landed further down the timeline than you scrolled.
Duplicate events
Every SDK call generates a freshmessageId, and the server dedupes on it — so SDK-level retries never double-count, but re-running a script re-sends everything as new messages. For imports you might run twice, use the HTTP API with deterministic message IDs, or track what you’ve already sent.
Browser only: an extension is blocking the request
Content and privacy extensions can block requests toin.glyphhq.io. If browser events are missing while server-side events arrive normally, check DevTools for ERR_BLOCKED_BY_CLIENT. There is no reliable client-side workaround; capture business-critical events server-side.
If you see this error, report blocked browser events. Include your workspace, browser, extension, and the affected event names so we can investigate.
Still stuck?
Email hello@glyph.app — a founder answers. Include the SDK and version, a snippet of how you initialize it, and anystatus values your onError callback logged; those three usually pin it down in one reply.