Edge troubleshooting
Ordered by how often each one occurs, not by severity.
Start at the Edge, not at the console
Before the first successful sync the console can tell you nothing useful: Cloud has no extended public keys for you, so every chain is red with "No Edge has connected yet."
The Edge's own status page carries the actual reason and is served before any sync:
http://localhost:8787/ # status page, in a browser
GET /v1/edge/health # machine-readable
GET /v1/edge/lease # lease state and epoch
GET /v1/edge/outbox # depth, and whether it is poisoned
Then the log:
docker logs --tail 200 chainos-edge
The container will not start
mnemonic file not readable
The container's uid cannot read the mounted file.
docker exec chainos-edge ls -l /run/secrets/chainos_mnemonic # if the image had a shell
stat -c '%a %U %G' ./secrets/mnemonic.txt # on the host
The image is scratch and has no shell, so check on the host: mode 0400 and owned by the
uid the container runs as (10001 by default). A file owned by root at 0400 is unreadable to a
non-root container, which is the most common form of this.
invalid BIP-39 checksum
The phrase is not a valid mnemonic. In order of likelihood: a trailing newline included in the phrase, a typo in one word, or two words transposed.
# A trailing newline is the usual culprit — `printf`, not `echo`.
xxd ./secrets/mnemonic.txt | tail -1
The Edge validates the checksum at boot and refuses rather than continuing, which is deliberate: a mnemonic with a typo derives perfectly valid addresses that you do not hold the keys to.
EDGE_DATA_DIR is on ephemeral storage
The data directory is a tmpfs or an emptyDir. The Edge refuses, because an outbox that loses
its contents on restart looks durable and is not.
Use a named Docker volume or a StatefulSet with a volumeClaimTemplate. If you genuinely
accept the loss, set EDGE_OUTBOX_MODE=disabled explicitly and read
what else that costs.
mlock failed
The Edge locks the memory region holding the seed so it cannot be paged to swap, and fails rather than continuing with a swappable seed.
- Docker: add
--cap-add IPC_LOCK, or raise--ulimit memlock. - Kubernetes:
securityContext.capabilities.add: ["IPC_LOCK"]. - Host: check
ulimit -l; a lowmemlocklimit on the host caps what the container can lock regardless of capabilities.
API_KEY has an unrecognised prefix
The key must begin flk_live_ or flk_test_. A zkl_* key is one the Edge issues to your
application — it is not a Ziklag credential and cannot be used here. See
API conventions.
It starts but never syncs
503 UPSTREAM_UNAVAILABLE in the log
The Edge cannot reach Cloud. Every connection is outbound from the Edge, so this is egress:
# From the Edge's network namespace or an equivalent pod
curl -sv https://api.chainos.cloud/actuator/health
Check the egress proxy, the allowlist, and TLS interception — a corporate MITM proxy breaks the
mTLS channel specifically, and it fails at handshake rather than with a clear message. If
CHAINOS_CLOUD_URL is set at all, check it: it exists for staging and a stale value left in a
production manifest points the Edge at nothing.
409 ALREADY_ENROLLED
Cloud already has a completed enrolment for this identity and a full sync would register
nothing new. The Edge handles this automatically by switching to POST /v1/edge/resume, so a
single occurrence in the log during recovery is normal.
Repeated occurrences mean the Edge lost its local enrolment cache and is re-attempting a full
sync each boot. Usually that means the data directory is not actually persisting — check the
volume — or EDGE_OUTBOX_MODE=disabled, which has nowhere to cache it.
409 RESYNC_REQUIRED
Cloud has no enrolment for this Edge — restored from a backup, or a sync that died partway. The Edge drops its stale local copy and enrols again automatically. No action needed.
401 EDGE_SIGNATURE_SKEW
The Edge's clock is more than 300 seconds from Cloud's. Fix NTP on the host. This is not a ChainOS setting to relax; the window exists to bound replay.
401 EDGE_SIGNATURE_INVALID or 409 IDENTITY_ROTATION_PENDING
These are security events, not transient failures. A different mnemonic is mounted than the one Cloud pinned. Do not retry, do not restart in a loop, and do not approve the rotation without knowing why the identity changed.
Go to Identity rotation.
409 CROSS_ENVIRONMENT_IDENTITY
The same mnemonic is in use in both live and sandbox. Generate a separate sandbox phrase — see Environments.
It syncs but a chain is not ready
Check readiness rather than guessing:
curl -s $CLOUD/v1/edge/readiness -H "Authorization: Bearer $TOKEN" | jq
| State | Meaning | Address generation |
|---|---|---|
not_ready (red) | No extended public key registered, or for Solana and Stellar no pool | Blocked |
ready (green) | Registered and healthy | Allowed |
quarantined (amber) | Rotation detected, awaiting owner approval | Blocked |
pool_low (orange) | Pooled chains only — Solana or Stellar below 20% | Allowed, replenish requested |
A chain that is ready but whose balances return 503 CHAIN_UNAVAILABLE is a Cloud-side
problem: the chain RPC endpoint is down or not configured. Derivation needs no RPC, which is
why address issuance keeps working while balances do not. Note that a zero is never reported in
this case — a confident zero is indistinguishable from an emptied wallet.
Webhooks are not arriving
In this order:
- Is the Edge the leader?
GET /v1/edge/lease. Only the leader relays. A fleet with no leader delivers nothing — see High availability. - Is the outbox draining?
GET /v1/edge/outbox. A depth that only grows means your endpoint is refusing deliveries.outboxPoisonedmeans a single write keeps failing and is blocking the queue behind it. - Is your endpoint returning 2xx? Delivery history is in the console per webhook, with the status code and duration of every attempt.
- Are you on the retry ladder? Delivery, then 2m, 5m, 30m, then the dead-letter queue. A flapping endpoint can be up to 37 minutes behind before it dead-letters.
- Is it in the dead-letter queue? The console lists it with a replay action.
507 EDGE_OUTBOX_FULL means the local queue reached capacity. Fix the endpoint; the queue
drains in order once deliveries succeed.
Signing fails
| Error | Cause |
|---|---|
423 EDGE_OFFLINE | No live Edge replica. Also raised for Solana and Stellar address allocation when the pool is exhausted |
423 EDGE_NO_LEADER | Replicas are live but none holds the lease. Check for a partition |
INSUFFICIENT_FUNDS | Not enough of the asset being sent |
INSUFFICIENT_GAS | Enough of the asset, not enough native currency for the fee. The common one |
INSUFFICIENT_RESERVE | Nominally enough, but the send would breach a locked reserve (XRP, Solana, Stellar) |
DESTINATION_OPT_IN_REQUIRED | Stellar only. The destination holds no trustline for the asset, and only it can establish one |
422 CHAIN_REJECTED | The node refused the transaction. The node's reason is in error.details |
The three insufficient-funds codes are separate because they have three different remedies. See Withdrawals.
Diagnostics to collect before opening a ticket
docker logs --tail 500 chainos-edge > edge.log
curl -s http://localhost:8787/v1/edge/health > health.json
curl -s http://localhost:8787/v1/edge/lease > lease.json
curl -s http://localhost:8787/v1/edge/outbox > outbox.json
docker inspect chainos-edge \
| jq '.[0].Config.Env | map(select(startswith("PASS_PHRASE") == false))' > env.json
The jq filter drops PASS_PHRASE*. Check the output before you send it anyway — and if you
find your mnemonic in there, you are using PASS_PHRASE instead of PASS_PHRASE_FILE, which is
its own finding.
Include the meta.requestId from any failing API response. It is what lets support find the
request on our side.