> ## Documentation Index
> Fetch the complete documentation index at: https://www.glyphhq.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Events aren't showing up — work through the causes in order of likelihood.

The checks below are ordered by likelihood. Start at the top unless the write key's last-used value points to a later section.

## 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](#youre-looking-at-the-wrong-environment) down through [flushing](#the-process-exited-before-flushing).
* **Recent** — requests arrive but the data isn't where you're looking, or messages are being rejected. Jump to [wiring up `onError`](#youre-not-seeing-delivery-errors) and 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: true` adds 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:`) so `close()` 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 disable `autoFlush`, you own flushing yourself.

## You're not seeing delivery errors

The SDKs report delivery problems through `onError`; they do not throw them from `track`, `profile`, `group`, or `page`. Configure the callback before debugging delivery:

<CodeGroup>
  ```ts JavaScript theme={"dark"}
  const writeKey = process.env.GLYPH_WRITE_KEY;
  if (!writeKey) {
    throw new Error("GLYPH_WRITE_KEY is not set");
  }

  const glyph = new GlyphNode({
    writeKey,
    onError: (error, messages, { retryable, status }) => {
      console.error("Glyph delivery failed", { status, retryable });
    },
  });
  ```

  ```py Python theme={"dark"}
  def on_error(error, messages, context):
      print(f"Glyph delivery failed: status={context.status} retryable={context.retryable}")

  glyph = Glyph(write_key=os.environ["GLYPH_WRITE_KEY"], on_error=on_error)
  ```
</CodeGroup>

The `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](/docs/reference/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](/docs/reference/write-keys#test-environment-limits).

## `429` — you're being rate limited

Each write key has a budget (default **600 messages per minute**, sliding window). The SDKs handle occasional `429`s 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](/docs/guides/backfill#work-within-the-rate-limit).

## 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 fresh `messageId`, 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](/docs/reference/http-api#idempotency), or track what you've already sent.

## Browser only: an extension is blocking the request

Content and privacy extensions can block requests to `in.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](/docs/guides/what-to-capture#track-the-moments-that-carry-meaning).

If you see this error, [report blocked browser events](mailto:hello@glyph.app?subject=Blocked%20browser%20events). Include your workspace, browser, extension, and the affected event names so we can investigate.

## Still stuck?

Email [hello@glyph.app](mailto:hello@glyph.app) — a founder answers. Include the SDK and version, a snippet of how you initialize it, and any `status` values your `onError` callback logged; those three usually pin it down in one reply.
