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

# Troubleshooting

> Fixes for flags that don't update, always return the default, push conflicts, 401s, type errors, and more.

## Overview

Each section below is a question developers ask about shipd, with the likely causes, how to check each one, and the fix. Links point to the page that owns the full behavior.

Two checks help with most problems:

```ts TypeScript theme={null}
// src/lib/shipd.ts
export const shipd = createClient<FlagDefinitions>({
  // …
  onError: (error) => console.error("[shipd]", error),
});

// null means no snapshot has loaded yet.
console.log("snapshot version", shipd.getVersion());
```

```go Go theme={null}
log.Printf("shipd request error: %v", client.LastError())
result := client.Evaluate("new-checkout", shipd.EvaluationContext{})
if result.SnapshotVersion != nil {
    log.Printf("snapshot version: %d", *result.SnapshotVersion)
} else {
    log.Print("no snapshot loaded")
}
```

```bash Terminal theme={null}
shipd whoami   # Organization, project, and environment your CLI key targets
```

## Why isn't my flag updating?

| Cause | Check | Fix |
| - | - | - |
| You are waiting for the next poll | You did not pass `stream: true` | Wait up to `refreshIntervalMs` (30 seconds by default), or pass `stream: true` for realtime updates. |
| Realtime is off on Node.js 20 | `onError` logs `shipdit: WebSocket is not available in this runtime` | Pass a `WebSocket` constructor. See [WebSocket is not available in this runtime](#websocket-is-not-available-in-this-runtime). |
| The SDK key belongs to another environment | Find the key on the project's **SDK keys** page and compare its environment with the one you edited | Use an SDK key created for the environment you are changing. See [SDK keys](/concepts/keys). |
| You changed a different environment | `shipd whoami` shows the environment `shipd push` targets; the dashboard shows the active environment | Push with `--env <environment>`, or switch environments in the dashboard and save again. |
| The dashboard change was not saved | Reload the flag page; unsaved edits are gone | Make the change again and click **Save environment**. |
| The change is waiting for approval | The flag has a pending request in **Approvals** | Get the request reviewed. See [Review production changes](/operations/approvals). |
| The flag is disabled | **Enabled in `<environment>`** is off | A disabled flag skips all rules and serves the **Fallthrough** value. To turn a feature off, set **Fallthrough** to `false`. See [Kill switch](/flags/kill-switch). |
| An earlier rule matches first | Test the user in the flag page simulator | Rules run top to bottom and the first match wins. Reorder the rules or narrow the earlier one. See [Target users](/flags/targeting). |

For Go, confirm `DisablePolling` is false and check `LastError()`. Use a bounded context with `Refresh(ctx)` to check a request immediately. Go does not use the WebSocket options in the TypeScript checks above. See [Go reference](/reference/go-sdk).

## Why does my flag always return the default?

| Cause | Check | Fix |
| - | - | - |
| You evaluate before the first snapshot loads | `shipd.getVersion()` returns `null` | `await shipd.ready()` at startup, or use `useReady()` in React. |
| The key is misspelled or does not exist | The SDK emits an `unregistered_flag` event; `shipd flags list` does not show the key | Fix the key, or run `shipd push` to create the flag. Generated types catch typos at compile time. |
| The SDK key is for another project or environment | Compare the key's project and environment on the **SDK keys** page | Use a key for the project and environment that contains the flag. |
| The edge rejects the key | `onError` reports a `401` | See [Why does the edge return 401?](#why-does-the-edge-return-401) |
| The rule needs a bucket attribute | The percentage condition uses `userId` (the default) or another attribute that is missing from your evaluation context | Pass the configured bucket attribute. In browsers, `identify()` also sets `userId`. |
| The flag was archived | The [audit log](/operations/audit-log) shows the archive, from the dashboard or `shipd push --prune` | Archived flags are removed from the snapshot, so the SDK serves your `defaults`. Remove the check from your code. |

The SDK never throws from an evaluation method. When it cannot resolve a value, it serves your `defaults`, or `false`, `"control"`, `0`, or `{}` if you did not pass one. See [SDK](/reference/sdk).

## Why did shipd push return 409 / exit 3?

**Cause.** `shipd push` exits with code `3` when the control plane returns `409 conflict`. Usually someone changed the flag after your last `shipd pull`. It also happens when a pending approval request already exists for the change, or when the push contains an operation that a policy-protected environment does not support.

**Check.** Run `shipd diff` to see how the server differs from your `shipd.config.ts`.

**Fix.** Run `shipd pull`, merge your change, and push again. See [Resolve a push conflict](/config-as-code/conflicts) and [Errors](/reference/errors).

## shipd push exited with code 5

**Cause.** An approval policy accepted part of the push but queued it for review. Exit code `5` means `pending_approval`: the queued changes are not live yet.

**Check.** Run the push with `--json` and read `approvalRequests`, or open **Approvals** in the dashboard.

**Fix.** Ask an eligible reviewer to approve the request. `--yes`, `--ci`, and `--force` do not bypass approval. Pushing again while the request is pending returns exit code `3`. See [Review production changes](/operations/approvals).

## Why does the edge return 401?

**Cause.** The edge returns the same generic `401` for any credential it does not accept:

* The SDK key was revoked.
* The key is a management API key (`shipd_mgmt_…`) or service account token (`shipd_sa_…`). Only SDK keys work on the edge.
* The key was truncated or has extra whitespace.

**Check.** Confirm the key starts with `shipd_sdk_server_` or `shipd_sdk_client_`, and that it shows **Active** on the project's **SDK keys** page.

**Fix.** Create a new SDK key and deploy it. After a `401`, the SDK keeps its last good snapshot and reports the error to `onError`. See [SDK keys, management API keys, and service account tokens](/concepts/keys) and [Edge API](/reference/edge-api).

## TypeScript says my flag key is not assignable

**Cause.** The flag key is not in your generated `FlagDefinitions`, or it is the wrong flag type for the method (for example, `isEnabled` on a variant flag).

**Check.** Open `src/generated/flags.ts` (or your configured `typesOut`) and search for the key. Check that `FlagDefinitions` is imported from that generated file and passed to `createClient<FlagDefinitions>()` or `createShipdReact<FlagDefinitions>()`.

**Fix.** Regenerate types:

```bash Terminal theme={null}
shipd types gen
```

`shipd push` and `shipd pull` also regenerate them. Use the method that matches the flag type: `isEnabled` for boolean, `getVariant` for variant, `getNumber` for number, and `getJson` for JSON flags. See [Generated types](/config-as-code/types).

## Percentage rollout serves everyone the same value

**Cause.** A percentage condition buckets by `userId` (or a chosen attribute). Without it, the condition never matches, so every request falls through to the same value.

**Check.** Log the evaluation context you pass. TypeScript server clients have no `userId` unless you pass one per call or in `createClient({ context })`. Go requires `EvaluationContext` per call and has no client-wide context option. Server `identify()` / `Identify` does not set evaluation context.

**Fix.** Pass a stable `userId`:

```ts TypeScript theme={null}
shipd.isEnabled("new-checkout", { userId: "usr_8f2k19" });
```

```go Go theme={null}
client.IsEnabled("new-checkout", shipd.EvaluationContext{UserID: "usr_8f2k19"})
```

See [Roll out a flag to a percentage of users](/flags/percentage-rollouts) and [Identify users](/flags/identify-users).

## WebSocket is not available in this runtime

**Cause.** You passed `stream: true`, but the runtime has no global `WebSocket`. Node.js 20 does not; Node.js 22 and later, and browsers, do.

**Check.** `onError` receives `shipdit: WebSocket is not available in this runtime`. Polling still works.

**Fix.** Upgrade to Node.js 22, pass a constructor from the `ws` package, or omit `stream` to poll only:

```ts src/lib/shipd.ts theme={null}
import WebSocket from "ws";

export const shipd = createClient<FlagDefinitions>({
  // …
  stream: true,
  WebSocket,
});
```

See [Node.js and server runtimes](/sdks/node).

## Flags flash in the browser on load

**Cause.** The page renders with your `defaults`, then re-renders when the snapshot arrives.

**Check.** The flash happens on a user's first visit, or after you set `persist: false`.

**Fix.** Pick one:

| Option | How |
| - | - |
| Wait for the snapshot | In React, render a placeholder until `useReady()` returns `true`. Without React, `await shipd.ready()` before rendering. |
| Keep the persisted cache | Leave `persist` on (the default). Returning visitors start from the snapshot saved in `localStorage`. |
| Bootstrap from the server | Fetch the snapshot on the server and pass it as `bootstrap`. The client is ready immediately. |

See [Browser](/sdks/browser), [React](/sdks/react), and [Next.js](/sdks/nextjs).

## What happens if shipd is down?

Flag checks keep working. Evaluation is local, so SDKs serve the last good snapshot while the edge is unreachable, and continue polling if the WebSocket gateway is down, provided polling is enabled. If the control plane is down, `shipd push` and the dashboard fail, but evaluation continues from the last published snapshot. See [What happens if shipd is down?](/concepts/local-evaluation#what-happens-if-shipd-is-down)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.