Error reference
This page is a reference for the errors you may encounter when working with Nakama, so you can identify what an error means, whether it’s expected, and what to check next.
Nakama exposes its API over both gRPC and HTTP. Clients receive errors from two layers:
- Transport and networking errors from the gRPC/HTTP layer, such as a canceled request or a timed-out connection.
- Application errors from the server, carrying a gRPC status code and a human-readable message.
Server framework code (Go, TypeScript, and Lua modules) also returns a set of predefined runtime error values, listed at the end of this page.
When you’re debugging, the gRPC status code is the stable signal to branch on, but the detail you need is often in the server log rather than the client response. Several errors below return a generic message to the client while recording the specific reason in the log.
gRPC status codes and HTTP mappings #
Every Nakama API error carries a gRPC status code. When a request arrives over HTTP, Nakama maps that code to an HTTP status code. The mapping is the gRPC-gateway default and is stable across Nakama releases.
| gRPC code | Number | HTTP status | Meaning |
|---|---|---|---|
OK | 0 | 200 OK | The request succeeded. |
Canceled | 1 | 499 Client Closed Request | The caller canceled the request before it completed. |
Unknown | 2 | 500 Internal Server Error | An unknown error, often an unhandled exception on the server. |
InvalidArgument | 3 | 400 Bad Request | A field in the request was missing or malformed. |
DeadlineExceeded | 4 | 504 Gateway Timeout | The request ran longer than its allotted timeout. |
NotFound | 5 | 404 Not Found | The requested resource doesn’t exist. |
AlreadyExists | 6 | 409 Conflict | The resource the request tried to create already exists. |
PermissionDenied | 7 | 403 Forbidden | The caller isn’t authorized to perform the operation. |
ResourceExhausted | 8 | 429 Too Many Requests | A resource quota or rate limit was reached. |
FailedPrecondition | 9 | 400 Bad Request | The system isn’t in the state required to run the operation. |
Aborted | 10 | 409 Conflict | The operation was aborted, typically due to a concurrency conflict. |
OutOfRange | 11 | 400 Bad Request | The operation was attempted past the valid range. |
Unimplemented | 12 | 501 Not Implemented | The operation isn’t implemented or supported. |
Internal | 13 | 500 Internal Server Error | An unexpected internal server error. |
Unavailable | 14 | 503 Service Unavailable | The service is currently unavailable, often transient. |
DataLoss | 15 | 500 Internal Server Error | Unrecoverable data loss or corruption. |
Unauthenticated | 16 | 401 Unauthorized | The request lacks valid authentication credentials. |
The status code tells you the category of problem. The codes you see most often from application logic are InvalidArgument (a malformed or missing field in your request), NotFound (the requested resource doesn’t exist), and Internal (an unexpected server-side failure).
Per-endpoint error messages #
Alongside the status code, most API errors carry a human-readable message describing what specifically went wrong. A bad pagination value, for example, returns Invalid limit - limit must be between 1 and 100.
These messages are defined per endpoint rather than centrally. They live in the api_*.go files of the Nakama server source, such as server/api_storage.go and server/api_account.go, and there are several hundred of them across the API. Because each call site defines its own wording, the same category of problem can be phrased differently by different endpoints.
Treat these messages as diagnostic text for people, not as a stable API contract. Branch on the gRPC status code instead, which is stable across releases. To trace a message back to its origin, search the server/api_*.go files for the message text.
Some operations don’t expose a distinct status code for every outcome. When a client joins a match that no longer exists, for example, the reliable signal is the match not found message rather than a dedicated code. Where an operation lacks a specific code for a case you need to detect, match on the documented message and treat the status code as the coarse category.
Common transport and networking errors #
These errors come from the network or the gRPC layer rather than from Nakama’s application logic. Many are expected under normal operating conditions and don’t indicate a bug in your integration or in the server.
Context canceled #
The context canceled error (gRPC Canceled, code 1) means the caller ended the request before it completed. This is one of the most common errors, and it’s usually expected: a client closed the connection, navigated away, or canceled the operation while the request was in flight. Over gRPC the full text is rpc error: code = Canceled desc = context canceled.
A frequent and benign form is a storage read logged as Could not read storage objects. with "error": "context canceled". This means the client disconnected before its database query returned. It’s safe to ignore unless it coincides with database performance problems.
What to check: confirm the affected requests are ones a client would abandon, such as a screen the player navigated away from or a request behind a flaky connection. Investigate only when cancellation hits requests that should always run to completion, or when you see a sustained spike, which points to database pressure or a client that cancels too aggressively.
Context deadline exceeded #
The context deadline exceeded error (gRPC DeadlineExceeded, code 4) means an operation ran longer than its allotted timeout. Like context cancellation, it’s common and often expected.
A frequent source is an outbound HTTP call from your own runtime code to an external service, such as Satori, an in-app purchase validation endpoint, or a third-party API. These surface as context deadline exceeded (Client.Timeout exceeded while awaiting headers), which means your HTTP client’s own timeout elapsed while waiting on the other service. The fix is usually to raise that client’s timeout, not a Nakama setting.
What to check: identify which operation is timing out. If it’s an outbound call from your module, raise that HTTP client’s timeout and check the external service’s latency. If it’s a database operation, check query duration and load such as IOPS and hot rows. Tune Nakama timeouts only once you’ve ruled out a slow dependency.
Network I/O timeout #
An I/O timeout (for example, read tcp ... i/o timeout) means a network read or write didn’t complete in time. This is most often caused by an unreliable client connection (a device on a poor mobile network, for example) rather than a problem on the server side.
Treat isolated I/O timeouts as a symptom of network conditions. Investigate only if they’re widespread across many clients at once, which can indicate a server or infrastructure issue.
SSL handshake failure #
An SSL or TLS handshake failure means the client and server couldn’t establish a secure connection. This is typically a TLS configuration or connectivity issue between the two parties, such as a certificate problem, a protocol mismatch, or a proxy interfering with the connection. It isn’t an application error.
The exact message depends on the client platform. Common forms are The SSL connection could not be established, Curl error 60: Cert verify failed, an SSL CA certificate error, and a TrustFailure or Handshake failed exception. A connection that works on one network but fails on another, for example failing on home broadband but working on mobile data, points to the network path (DNS, IPv6, a proxy, or an ISP) rather than the server.
What to check: confirm the certificate chain is trusted on the failing platform, that client and server agree on a TLS version, and whether the failure is specific to one network. A sudden onset across many clients can follow certificate rotation on a dependency such as Satori.
Storage version rejection #
A conditional write whose version check fails returns gRPC InvalidArgument (code 3, HTTP 400) with the message Storage write rejected. The server framework surfaces it as ErrStorageRejectedVersion. This is the expected outcome when another writer updated the object first, and it’s how Nakama prevents conflicting concurrent writes.
The server returns the same code and message when it rejects a write for insufficient permissions (ErrStorageRejectedPermission), so the response alone doesn’t tell you which of the two applied. A conditional delete that fails its version check returns Storage delete rejected. with the same code.
When you use Hiro, the same rejection appears in server logs as an nk.MultiUpdate error carrying Storage write rejected - version check failed., from the economy, inventory, or achievements systems. It means two operations wrote the same object concurrently, for example an achievement claim and an achievement update running at once, or a client retry replaying a write with a stale version.
What to check: look for two writes to the same object overlapping in time. Common sources are concurrent Hiro operations on one user, an offline queue replaying writes on reconnect, and account creation or linking hooks that edit a wallet which already exists. To reduce it, batch updates to the same object into one call, sequence operations that touch the same object, and retry a rejected write with jitter after re-reading the current version.
For how conditional writes work and how to handle a rejected version, see Conditional writes .
Socket heartbeat disconnects #
Nakama keeps realtime socket connections alive with a heartbeat. The server sends a WebSocket ping every socket.ping_period_ms (15 seconds by default) and expects a pong back within socket.pong_wait_ms (25 seconds by default). If no pong or other message arrives in that window, the server treats the connection as dead and closes it.
The most common cause of an unexpected heartbeat disconnect is a blocked client rather than a network fault. A WebSocket library answers pings automatically, but only while the client keeps servicing its network loop. Long blocking work on that same thread, such as loading assets or running a synchronous scene transition, holds the pong back until the work finishes. If that takes longer than socket.pong_wait_ms, the server has already closed the connection.
To avoid this, keep servicing the socket during long operations, move blocking work off the thread that drives the socket, or split it into chunks that yield frequently. Raising socket.pong_wait_ms buys headroom but doesn’t remove the underlying stall. Always handle the disconnect event and reconnect, because connections also drop for ordinary network reasons.
A client that sends messages regularly doesn’t need a ping to prove it’s alive. The server skips one when the client sent at least socket.ping_backoff_threshold messages (20 by default) during the ping period.
Social authentication errors #
A failed social login returns a generic message to the client rather than the reason the identity provider gave. Authenticating against Google, Apple, Facebook, Game Center, or Steam returns gRPC Unauthenticated (HTTP 401) with a message that names only the provider, such as Could not authenticate Google profile.
The specific reason comes from the provider, and Nakama writes it to the server log instead of returning it to the client. Typical reasons include an invalid or expired ID token, an unexpected issuer claim, and a failed certificate lookup. Nakama doesn’t generate these reasons, so repeated failures usually point to provider-side credentials or configuration rather than to Nakama.
Debug a social authentication failure from the server log entry recorded with the request. The client message alone can’t tell you which reason applied.
Runtime error values #
The server framework defines a fixed set of error values in nakama-common. Runtime operations return these, so you can match against them in your Go, TypeScript, and Lua modules. The common ones you’re likely to act on, such as storage version rejection, are explained in the sections above; the table below is the complete list.
The messages are intentionally terse and often restate the error name, so the value itself rarely tells you what to do next. Match the error rather than its message text (in Go, with errors.Is), then follow the What to do next column, which explains when each error fires and how to handle it. For the fuller flow, see the concept guide for the system the error belongs to.
| Error | Message | What to do next |
|---|---|---|
ErrCannotDecodeParams | error creating match: cannot decode params | Nakama couldn’t decode the stored match parameters. Make sure the params you created the match with are serializable values. |
ErrCannotEncodeParams | error creating match: cannot encode params | Nakama couldn’t encode the parameters passed to match create. Pass only serializable values in the params map. |
ErrChannelCursorInvalid | invalid channel cursor | The pagination cursor doesn’t match this channel query. Pass back the cursor from the previous page unchanged, or omit it to start from the first page. |
ErrChannelGroupNotFound | group not found | The group behind this channel doesn’t exist. Confirm the group ID and that the group hasn’t been deleted. |
ErrChannelIDInvalid | invalid channel id | The channel ID is malformed. Build it from a valid channel target instead of composing the string by hand. |
ErrDeferredBroadcastFull | too many deferred message broadcasts per tick | Too many messages were queued for broadcast in a single match tick. Send fewer messages per tick, or spread them across ticks. |
ErrFriendInvalidCursor | friend cursor invalid | The friend list pagination cursor is invalid. Reuse the cursor from the previous page, or omit it to start over. |
ErrGracePeriodExpired | grace period expired | A graceful shutdown’s grace period elapsed before this operation finished. Expected during shutdown or redeploys: make long operations idempotent and safe to retry so the client can repeat them after reconnecting. |
ErrGroupCreatorInvalid | group creator user ID not valid | The creator user ID isn’t a valid UUID. Pass the ID of an existing user. |
ErrGroupFull | group is full | The group is at its member limit. Handle it as full, or raise its max count. |
ErrGroupLastSuperadmin | user is last group superadmin | The last superadmin can’t leave or be demoted. Promote another member to superadmin first. |
ErrGroupNameInUse | group name in use | Another group already uses this name. Pick a unique name. |
ErrGroupNoUpdateOps | no group updates | The update request didn’t change any fields. Include at least one field to update. |
ErrGroupNotFound | group not found | No group exists with this ID. Check the ID and that the group hasn’t been deleted. |
ErrGroupNotUpdated | group not updated | The group update didn’t apply. Confirm the group exists and that the field values are valid. |
ErrGroupPermissionDenied | group permission denied | The caller lacks permission for this group operation. Check their group role, since only admins and superadmins can manage members and settings. |
ErrGroupUserInvalidCursor | group user cursor invalid | The group members pagination cursor is invalid. Reuse the cursor from the previous page, or omit it to start over. |
ErrGroupUserNotFound | user not found | The target user isn’t a member of this group. Check the user ID and membership. |
ErrInvalidChannelTarget | Invalid channel target | The channel target is empty or malformed. Pass a user ID, group ID, or room name that matches the channel type you’re joining. |
ErrInvalidChannelType | Invalid channel type | The channel type isn’t a supported value. Use a room, direct message, or group channel type. |
ErrLeaderboardNotFound | leaderboard not found | No leaderboard exists with this ID. Create it at server startup, or check the ID for typos. |
ErrMatchBusy | match busy | The match’s processing queue is full, so it can’t take this call right now. Retry shortly, and reduce how often you call into a single match. |
ErrMatchIdInvalid | match id invalid | The match ID is malformed. Use an ID returned by Nakama rather than composing one by hand. |
ErrMatchLabelTooLong | match label too long, must be 0-2048 bytes | The match label exceeds 2048 bytes. Shorten it. |
ErrMatchNotFound | match not found | No match exists with this ID. It may have ended or never started. On reconnect, treat this as the match no longer being available and route the player elsewhere. |
ErrMatchStateFailed | match did not return state | A match handler callback didn’t return state. Return the state value from every match loop callback, including when nothing changed. |
ErrMatchmakerDelete | matchmaker delete error | The matchmaker couldn’t remove the ticket. Confirm the ticket ID still exists, then retry. |
ErrMatchmakerDuplicateSession | matchmaker duplicate session | The session already has an active ticket in this matchmaker. Cancel or reuse the existing ticket instead of adding another. |
ErrMatchmakerIndex | matchmaker index error | An internal matchmaker indexing error occurred. Retry the request, and check the server logs if it persists. |
ErrMatchmakerNotAvailable | matchmaker not available | The matchmaker isn’t ready to serve requests, usually briefly during startup or a cluster change. Retry after a short delay, and check cluster health if it continues. |
ErrMatchmakerQueryInvalid | matchmaker query invalid | The matchmaker query is malformed. Check the query syntax and the properties it references. |
ErrMatchmakerTicketNotFound | matchmaker ticket not found | No matchmaker ticket matches this ID for the session. Check the ticket ID, and note it may already have matched or been canceled. |
ErrMatchmakerTooManyTickets | matchmaker too many tickets | The session is over its matchmaker ticket limit. Cancel unused tickets before adding new ones. |
ErrPartyAcceptRequest | party could not accept request | The join request couldn’t be accepted, usually because the party filled or the request was withdrawn. Re-check the party state before retrying. |
ErrPartyClosed | party closed | The party is closed to open joins. Only invited users can join, so send an invite or open the party. |
ErrPartyFull | party full | The party is at its member limit. Handle it as full, or create the party with a larger max size. |
ErrPartyJoinRequestAlreadyMember | party join request already member | The user is already in the party, so no join request is needed. |
ErrPartyJoinRequestDuplicate | party join request duplicate | A join request from this user is already pending. Wait for the leader to respond rather than resending. |
ErrPartyJoinRequestsFull | party join requests full | The party’s pending join requests are at capacity. Have the leader accept or reject some before more can arrive. |
ErrPartyLabelTooLong | party label too long | The party label exceeds the size limit. Shorten it. |
ErrPartyNotLeader | party leader only | This operation is leader-only. Perform it as the party leader. |
ErrPartyNotMember | party member not found | The target user isn’t in the party. Check the user ID and current membership. |
ErrPartyNotRequest | party join request not found | No pending join request matches this user. It may have been accepted, rejected, or withdrawn already. |
ErrPartyRemove | party could not remove | The member couldn’t be removed. Confirm they’re still in the party and that you’re the leader. |
ErrPartyRemoveSelf | party cannot remove self | A leader can’t remove themselves. Promote another member to leader first, or close the party. |
ErrSatoriConfigurationInvalid | satori configuration is invalid | Satori isn’t configured correctly. Check the Satori URL and API key in your Nakama configuration. |
ErrStorageRejectedPermission | Storage write rejected - permission denied. | The caller lacks write permission for this object. Check the object’s write permission and that the caller owns it, or perform the write as the system user from server code. |
ErrStorageRejectedVersion | Storage write rejected - version check failed. | Another writer updated the object first, so a conditional write lost the version check. Re-read the object, take its new version, and retry the write with jitter. See Conditional writes . |
ErrStorageWriteExhaustedRetries | Storage write retries exhausted. | Nakama retried a conditional write repeatedly and kept losing the version race. Reduce concurrent writes to the same object, or batch them into a single update call. |
ErrTournamentAuthoritative | tournament only allows authoritative submissions | The tournament only accepts server-authoritative submissions. Submit the score from your server runtime code, not from the client. |
ErrTournamentMaxSizeReached | tournament max size reached | The tournament is full. Handle it as full, or raise its max size when you create it. |
ErrTournamentNotFound | tournament not found | No tournament exists with this ID. Create it before writing a record, or check the ID. |
ErrTournamentOutsideDuration | tournament outside of duration | The tournament isn’t active right now. Check its start time, duration, and reset schedule before submitting. |
ErrTournamentWriteJoinRequired | required to join before writing tournament record | The player must join the tournament before submitting a score. Call the join operation first. |
ErrTournamentWriteMaxNumScoreReached | max number score count reached | The player used every submission allowed in the active period. Wait for the next reset, or raise the tournament’s max score count. |
ErrUserGroupInvalidCursor | user group cursor invalid | The user’s groups pagination cursor is invalid. Reuse the cursor from the previous page, or omit it to start over. |
ErrWalletLedgerInvalidCursor | wallet ledger cursor invalid | The wallet ledger pagination cursor is invalid. Reuse the cursor from the previous page, or omit it to start over. |
