Troubleshooting
Common error codes and what to do about them.
# When in doubt, bump the log level and re-run the failing command.
parsec serve --addr :8000 --state-dir /var/lib/parsec
# Use the LOG_LEVEL env var (the slog default handler honors it) for
# debug-level structured output.
LOG_LEVEL=debug parsec serve --addr :8000
Every error that crosses a package boundary carries a PARSEC_* code.
The codes are stable; the messages are not. Match on the code.
PARSEC_AUTH_DENIED
The mgmt bearer is missing, malformed, or signed by a key the ring no longer trusts. Causes, in order of likelihood:
- You forgot
--token/PARSEC_TOKEN. Set it and retry. - The token’s
kidresolves to a retired key. Mint a fresh bearer:parsec tokens mgmt. - The token’s
algortypclaim was tampered with. Parsec only acceptsHS256/JWTwith akid. - The keyring was rotated and the old key retired. See key rotation.
Note: a closed channel returns PARSEC_CHANNEL_CLOSED, NOT
PARSEC_AUTH_DENIED. If you see denied on a subscribe, the token is
the problem.
PARSEC_AUTH_EXPIRED
The token’s exp claim is in the past. Mint a new one. For access
tokens, use the refresh flow:
parsec tokens refresh "<refresh-token>"
For mgmt tokens, mint a fresh bearer under the active key:
parsec tokens mgmt --ttl 24h
PARSEC_CHANNEL_INVALID
The name does not match the grammar enforced by
channels.ParseName. See naming. Frequent
gotchas:
- Missing
public:/private:prefix. - Uppercase or non-ASCII components.
- A
:inside the body (the colon is reserved for the visibility prefix). - Private channel without an id segment.
PARSEC_CHANNEL_NOT_FOUND
The channel either:
- Was never opened.
- Was a private channel whose TTL elapsed and was reaped on the next sweep (default sweep interval: 30s).
- Was deleted explicitly.
- Lived in a process that has since restarted (channel state is in-memory).
Reopen public channels with channels open; private channels need to
be re-created and a fresh token pair handed out.
PARSEC_CHANNEL_CLOSED
Public channel whose inactivity TTL elapsed. The record is still there; new subscribes are refused. Reopen:
parsec channels open public:<name> --ttl 1h
This resets LastActive and emits a fresh opened event.
PARSEC_CHANNEL_EXISTS
You called channels create on a private name that already exists.
Private channels do NOT support reopen — the API only goes one way.
Pick a different id segment or wait for the existing channel to
expire.
PARSEC_CHANNEL_TTL_EXCEEDS_MAX
You requested a private channel with ttl > 1h. The cap is in
channels.MaxPrivateTTL. Lower the TTL and retry. If you actually
need a long-lived feed, consider whether it should be public.
PARSEC_BROKER_NOT_READY
The Centrifuge node has not finished node.Run() yet. Almost always a
boot race — the server received an RPC before it finished initializing
the broker. Retry after a short backoff; this usually clears in under
a second.
PARSEC_BROKER_INTERNAL
The OSS Centrifuge library returned an error from Publish,
Presence, or Run. Check the server log; the underlying message is
preserved in the wrapped error chain.
PARSEC_SINK_UNAVAILABLE
A sink’s Send failed at the transport layer. Common causes:
- SMTP server refused the relay (email sink).
- Slack webhook returned a 4xx/5xx.
- Webhook target was unreachable.
Parsec does NOT retry. Wrap the sink in your own retry layer or queue
the message upstream of PublishOrSink.
PARSEC_SINK_CONFIG_INVALID
A sink rejected its constructor config. Re-read the sink’s documentation:
PARSEC_INVALID_ARGUMENT
Generic input validation failure. The accompanying message is the useful part — read the server log or the response body.
PARSEC_INTERNAL
Unhandled, unexpected error path. File a bug with the surrounding log context. These should be rare; treat them as actionable.
Redis and keyring symptoms
These are configuration failures rather than coded errors, so they show up
in the log or as behavior rather than as a PARSEC_* response.
redis address ... is neither host:port nor a redis URL
The address is malformed. Supported forms are host:port and redis://,
rediss://, tcp:// or unix:// URLs. parsec serve fails at startup
rather than dialing a host that does not exist.
redis address ... uses redis+sentinel, which the parsec redis client cannot build
Sentinel and cluster topologies need a client parsec cannot construct from
an address alone. Build one yourself and pass Options.RedisClient; the
CLI cannot express this.
redis unreachable at <addr>
Redis did not answer a ping within RedisPingTimeout (5s default). Boot
fails deliberately — the alternative is a process that answers /healthz
while every operation fails. Check the address, credentials, and any
network policy between the two.
Tokens minted on one node are rejected by another
The nodes are not sharing a keyring. Check
parsec_keyring_backend{backend="redis"} is 1 on every node: a node
reporting file or ephemeral bootstrapped its own keys. Then compare
parsec_keyring_version across nodes — equal numbers mean they agree.
A rotation on one node never reaches another
Look at parsec_keyring_watch_up on the lagging node. At 0 it is between
watcher restarts and cannot receive events; parsec_keyring_reconcile_errors_total
climbing alongside means its reads are failing too, which points at Redis
connectivity rather than at parsec. A single missed event is not permanent:
each node re-reads the ring every --keyring-poll.
Every token suddenly fails after a Redis restart
Redis lost the keyring and parsec bootstrapped a fresh one — an empty
keyspace is indistinguishable from a first boot. The log line is
bootstrapped a new keyring in redis where you expected loaded keyring from redis. Fix the persistence settings
(Redis durability) and restore from a
parsec keys export backup if you have one. Without a backup, every
client has to re-authenticate.
parsec: StateDir is set but redis holds the keyring
Not an error — a warning that keyring.json is neither read nor written
because Redis wins the keyring precedence. Drop state_dir from the config
to make the deployment say what it means, and make sure Redis is actually
durable.
Debug tips
- Run
parsec channels listto confirm what the server thinks exists. - Run
parsec channels get <name>to inspect a single record’s state andlast_active. - Use
parsec --json(or curl/manifest) to verify the server version, configured sinks, and persistence stance. - Watch the server log with the slog default handler on debug; key
events (
keyring reload,bootstrap mgmt token) are emitted as structured events. - Scrape
/metricsand grepparsec_keyring_to see which backend a node is using, which keyring revision it has, and whether its watcher is up. See keyring health.