# LobbyLab error codes

Every error LobbyLab reports has a stable code and a page that says what it means and what to do. Codes never change meaning once shipped.

## Multiplayer API errors

Sent in the JSON error body with the HTTP status shown. Status: the Multiplayer API is in private alpha.

| Code | HTTP | When |
|---|---|---|
| [`room_type_unknown`](/docs/errors/room_type_unknown) | 400 | The request names a room type this project does not have. |
| [`invalid_request`](/docs/errors/invalid_request) | 400 | The body, query or a header does not match the route's schema. `detail` names the field. |
| [`key_invalid`](/docs/errors/key_invalid) | 401 | The key is unknown or malformed. |
| [`key_revoked`](/docs/errors/key_revoked) | 401 | The key was revoked. |
| [`project_expired`](/docs/errors/project_expired) | 401 | A sandbox project is older than 72 hours. |
| [`token_invalid`](/docs/errors/token_invalid) | 401 | The player token is malformed, signed for another project, or not a token. |
| [`token_expired`](/docs/errors/token_expired) | 401 | The player token is past its expiry. |
| [`project_disabled`](/docs/errors/project_disabled) | 403 | The project's kill switch is on. |
| [`origin_not_allowed`](/docs/errors/origin_not_allowed) | 403 | The request's Origin is not in the project's list of allowed origins. |
| [`native_anonymous_disabled`](/docs/errors/native_anonymous_disabled) | 403 | A publishable key was used without an Origin (a native app or a server) and the project does not allow anonymous native play. |
| [`secret_key_in_browser`](/docs/errors/secret_key_in_browser) | 403 | A secret key arrived with an Origin header: it was sent from a browser. |
| [`anonymous_disabled`](/docs/errors/anonymous_disabled) | 403 | The project turned anonymous play off. |
| [`room_locked`](/docs/errors/room_locked) | 403 | The host locked the room: it takes no new members. Seated members can still reconnect. |
| [`banned`](/docs/errors/banned) | 403 | This player was banned from the room. |
| [`not_reserved`](/docs/errors/not_reserved) | 403 | The room was created for chosen players only, and this player is not one of them. |
| [`not_a_member`](/docs/errors/not_a_member) | 403 | A seat renewal or a leave for a room this player is not in (or was removed from). |
| [`rooms_limit_reached`](/docs/errors/rooms_limit_reached) | 403 | The project has as many rooms open at once as its plan allows. The answer carries `quota`. |
| [`ccu_limit_reached`](/docs/errors/ccu_limit_reached) | 403 | The project has as many players online at once as its plan allows. The answer carries `quota`. |
| [`player_rooms_limit_reached`](/docs/errors/player_rooms_limit_reached) | 403 | This player is already in 3 rooms. The answer carries `quota`. |
| [`capability_missing`](/docs/errors/capability_missing) | 403 | The player token does not grant what this route needs (its `can` list). |
| [`room_not_found`](/docs/errors/room_not_found) | 404 | No room has this id, or it closed and was deleted. |
| [`code_not_found`](/docs/errors/code_not_found) | 404 | No open room has this code. |
| [`not_found`](/docs/errors/not_found) | 404 | No route has this method and path. |
| [`method_not_allowed`](/docs/errors/method_not_allowed) | 405 | The path exists, but not with this HTTP method. |
| [`room_full`](/docs/errors/room_full) | 409 | The room has `maxPlayers` players, or it has seated 4 times `maxPlayers` members over its life. |
| [`idempotency_key_reused`](/docs/errors/idempotency_key_reused) | 409 | The same Idempotency-Key came with a different body. |
| [`payload_too_large`](/docs/errors/payload_too_large) | 413 | The request body is larger than the route accepts. |
| [`unsupported_media_type`](/docs/errors/unsupported_media_type) | 415 | The body is not `application/json`. |
| [`rate_limited`](/docs/errors/rate_limited) | 429 | A rate limit was hit. The answer carries `retryAfterSec` (also the Retry-After header) and `scope`, which names the limit. |
| [`internal_error`](/docs/errors/internal_error) | 500 | Something failed on LobbyLab's side. |
| [`realtime_capacity_limited`](/docs/errors/realtime_capacity_limited) | 503 | The server's pool of live rooms is full, or the Free and sandbox share of it is used up. The answer carries `retryAfterSec`. |
| [`sandbox_capacity_reached`](/docs/errors/sandbox_capacity_reached) | 503 | All sandboxes together are at their ceiling. The answer carries `retryAfterSec`. |

## Socket rejections

A change sent over the socket that the room refuses comes back as a `rejection` with one of these codes. Clients treat any other code as a generic refusal.

| Code | Meaning |
|---|---|
| `forbidden_path` | The room type's write rules do not let this player write that path. |
| `guard_failed` | A guard in the change did not hold: someone changed the value first. Read the new state and try again. |
| `version_conflict` | `ifVersion` did not match the room's version: the state moved on. Read it and try again. |
| `too_large` | The change would take the state, the view or a player slot past its size limit. |
| `rate_limited` | The player sent changes faster than the room allows. Send fewer (live rooms: use your player slot). |
| `invalid_op` | An operation is malformed (a bad path, `inc` on something that is not a number, and so on). |
| `not_host` | Only the host can do this. |
| `not_a_player` | Spectators cannot change the state. |
| `unknown_intent` | The intent name is not one the room knows. |

## Removed from a room

A member's sockets close with code `4403` and one of these reasons.

| Reason | Meaning |
|---|---|
| `left` | The player left the room. |
| `kicked` | The host or the game's backend removed the player. |
| `banned` | The player was removed and may not come back. |
| `room_closed` | The room closed. |
| `origin_not_allowed` | The socket's origin is not allowed for the project. |
| `project_disabled` | The project was turned off. |

## Build check problems

Problems the build check reports for a game package. Errors stop a game from being published; warnings do not.

### Package files

| Code | Severity | Problem |
|---|---|---|
| [`package.missing_directory`](/docs/errors/package.missing_directory) | error | The package folder does not exist |
| [`package.missing_file`](/docs/errors/package.missing_file) | error | A required package file is missing |
| [`package.not_a_file`](/docs/errors/package.not_a_file) | error | A package file is a folder or a link |
| [`package.file_too_large`](/docs/errors/package.file_too_large) | error | A package file is over its size limit |
| [`package.file_unreadable`](/docs/errors/package.file_unreadable) | error | A package file could not be read |
| [`package.unpublished_files`](/docs/errors/package.unpublished_files) | warning | client/ has files that are not published |
| [`package.client_pending`](/docs/errors/package.client_pending) | warning | Only client/index.html is missing |

### manifest.json

| Code | Severity | Problem |
|---|---|---|
| [`manifest.invalid_json`](/docs/errors/manifest.invalid_json) | error | manifest.json is not valid JSON |
| [`manifest.missing_field`](/docs/errors/manifest.missing_field) | error | manifest.json is missing a required field |
| [`manifest.unknown_field`](/docs/errors/manifest.unknown_field) | error | manifest.json has an unknown field |
| [`manifest.player_range`](/docs/errors/manifest.player_range) | error | maximumPlayers is below minimumPlayers |
| [`manifest.subscriptions`](/docs/errors/manifest.subscriptions) | error | subscriptions is missing a required event |
| [`manifest.bot_count`](/docs/errors/manifest.bot_count) | error | botCount or botsSetting does not fit the player count |
| [`manifest.execution_settings`](/docs/errors/manifest.execution_settings) | error | The realtime or timer settings do not fit the execution profile |
| [`manifest.smoke_intents`](/docs/errors/manifest.smoke_intents) | error | smokeIntents is empty or names a player who has not joined |
| [`manifest.smoke_inputs`](/docs/errors/manifest.smoke_inputs) | error | smokeInputs names a missing player or does not fit the profile |
| [`manifest.smoke_payload`](/docs/errors/manifest.smoke_payload) | error | A smoke payload is not small plain JSON |
| [`manifest.config_invalid`](/docs/errors/manifest.config_invalid) | error | config is not small plain JSON |
| [`manifest.too_large`](/docs/errors/manifest.too_large) | error | manifest.json is over its size limit |
| [`manifest.invalid_value`](/docs/errors/manifest.invalid_value) | error | A manifest.json field has a value that is not allowed |
| [`manifest.no_smoke_inputs`](/docs/errors/manifest.no_smoke_inputs) | warning | A realtime game has no smokeInputs |
| [`manifest.large_room_untested`](/docs/errors/manifest.large_room_untested) | warning | Rooms over 8 players are not simulated |

### server.ts rules

| Code | Severity | Problem |
|---|---|---|
| [`server.syntax_error`](/docs/errors/server.syntax_error) | error | server.ts has a TypeScript syntax error |
| [`server.import.forbidden`](/docs/errors/server.import.forbidden) | error | server.ts imports something other than @playable/sim |
| [`server.import.named_only`](/docs/errors/server.import.named_only) | error | @playable/sim is imported without named imports |
| [`server.import.reexport`](/docs/errors/server.import.reexport) | error | server.ts re-exports from another module |
| [`server.import.dynamic`](/docs/errors/server.import.dynamic) | error | server.ts uses import() or import.meta |
| [`server.import.require`](/docs/errors/server.import.require) | error | server.ts uses require() |
| [`server.forbidden_api.eval`](/docs/errors/server.forbidden_api.eval) | error | eval() or Function() in the rulebook |
| [`server.forbidden_api.globals`](/docs/errors/server.forbidden_api.globals) | error | process, globalThis, global, Bun or Deno in the rulebook |
| [`server.forbidden_api.network`](/docs/errors/server.forbidden_api.network) | error | Network access in the rulebook |
| [`server.forbidden_api.timers`](/docs/errors/server.forbidden_api.timers) | error | Timers in the rulebook |
| [`server.forbidden_api.date`](/docs/errors/server.forbidden_api.date) | error | Date or performance in the rulebook |
| [`server.forbidden_api.random`](/docs/errors/server.forbidden_api.random) | error | Math.random() in the rulebook |
| [`server.forbidden_api.reflection`](/docs/errors/server.forbidden_api.reflection) | error | .constructor, Reflect or Proxy in the rulebook |
| [`server.forbidden_api.console`](/docs/errors/server.forbidden_api.console) | error | console in the rulebook |
| [`server.rulebook_export`](/docs/errors/server.rulebook_export) | error | server.ts does not export rulebook |
| [`server.tick_missing`](/docs/errors/server.tick_missing) | error | A realtime rulebook has no tick method |
| [`server.tick_unused`](/docs/errors/server.tick_unused) | warning | tick is defined but the game is not realtime |

### Bundling server.ts

| Code | Severity | Problem |
|---|---|---|
| [`bundle.missing_export`](/docs/errors/bundle.missing_export) | error | An imported name does not exist in @playable/sim |
| [`bundle.build_error`](/docs/errors/bundle.build_error) | error | server.ts could not be bundled |
| [`bundle.failed`](/docs/errors/bundle.failed) | error | Bundling server.ts failed |
| [`bundle.too_large`](/docs/errors/bundle.too_large) | error | The bundled server is over its size limit |

### client/index.html rules

| Code | Severity | Problem |
|---|---|---|
| [`client.external_url`](/docs/errors/client.external_url) | error | The client has an external URL |
| [`client.protocol_relative_url`](/docs/errors/client.protocol_relative_url) | error | The client has a protocol-relative URL |
| [`client.script_src`](/docs/errors/client.script_src) | error | The client loads a script file other than the kit |
| [`client.legacy_runtime_conflict`](/docs/errors/client.legacy_runtime_conflict) | error | The client loads both the kit and the legacy 3D runtime |
| [`client.module_import`](/docs/errors/client.module_import) | error | An inline module script uses import or export |
| [`client.syntax_error`](/docs/errors/client.syntax_error) | error | An inline script has a syntax error |
| [`client.kit_not_loaded`](/docs/errors/client.kit_not_loaded) | error | Playable.connect is called but the kit is not loaded |
| [`client.not_connected`](/docs/errors/client.not_connected) | error | The client never connects to the platform |
| [`client.canvas2d_context`](/docs/errors/client.canvas2d_context) | error | A canvas2d game has no 2D canvas |
| [`client.webgl_renderer`](/docs/errors/client.webgl_renderer) | error | A webgl game has no 3D renderer |
| [`client.raw_webgl`](/docs/errors/client.raw_webgl) | error | A raw WebGL client misses a required check |
| [`client.navigation`](/docs/errors/client.navigation) | error | The client reloads or navigates its own page |
| [`client.raw_three_renderer`](/docs/errors/client.raw_three_renderer) | warning | The client creates THREE.WebGLRenderer directly |
| [`client.no_send_input`](/docs/errors/client.no_send_input) | warning | A realtime client never calls sendInput |
| [`client.legacy_bridge`](/docs/errors/client.legacy_bridge) | warning | A realtime client uses the legacy bridge |
| [`client.storage`](/docs/errors/client.storage) | warning | The client uses browser storage or cookies |
| [`client.parent_frame`](/docs/errors/client.parent_frame) | warning | The client touches the parent frame |
| [`client.network`](/docs/errors/client.network) | warning | The client opens its own network connection |

### client/assets

| Code | Severity | Problem |
|---|---|---|
| [`assets.invalid_name`](/docs/errors/assets.invalid_name) | error | An asset name has characters that are not allowed |
| [`assets.link`](/docs/errors/assets.link) | error | client/assets contains a link |
| [`assets.too_deep`](/docs/errors/assets.too_deep) | error | client/assets is nested too deep |
| [`assets.unsupported_type`](/docs/errors/assets.unsupported_type) | error | An asset has a file type that is not allowed |
| [`assets.budget_exceeded`](/docs/errors/assets.budget_exceeded) | error | The assets are over their count or size limits |
| [`assets.invalid_glb`](/docs/errors/assets.invalid_glb) | error | A .glb file is not valid binary glTF 2.0 |
| [`assets.glb_external_resource`](/docs/errors/assets.glb_external_resource) | error | A .glb file references external buffers or images |
| [`assets.directory_invalid`](/docs/errors/assets.directory_invalid) | error | client/assets is not a normal folder |
| [`assets.unreadable`](/docs/errors/assets.unreadable) | error | client/assets could not be read |

### The smoke run

| Code | Severity | Problem |
|---|---|---|
| [`smoke.init_rejected`](/docs/errors/smoke.init_rejected) | error | rulebook.init rejected the game setup |
| [`smoke.join_rejected`](/docs/errors/smoke.join_rejected) | error | A join was rejected in the smoke run |
| [`smoke.start_rejected`](/docs/errors/smoke.start_rejected) | error | The start was rejected in the smoke run |
| [`smoke.intent_rejected`](/docs/errors/smoke.intent_rejected) | error | A smoke intent was rejected |
| [`smoke.invalid_state`](/docs/errors/smoke.invalid_state) | error | The rulebook returned an invalid state |
| [`smoke.invalid_view`](/docs/errors/smoke.invalid_view) | error | rulebook.project returned an invalid view |
| [`smoke.invalid_event`](/docs/errors/smoke.invalid_event) | error | The rulebook returned an invalid event |
| [`smoke.too_many_events`](/docs/errors/smoke.too_many_events) | error | One transition returned too many events |
| [`smoke.threw`](/docs/errors/smoke.threw) | error | A rulebook method threw an error |
| [`smoke.missing_method`](/docs/errors/smoke.missing_method) | error | The rulebook is missing a method |
| [`smoke.not_json`](/docs/errors/smoke.not_json) | error | A rulebook method returned something that is not plain JSON |
| [`smoke.output_too_large`](/docs/errors/smoke.output_too_large) | error | A rulebook method returned more data than its budget |
| [`smoke.random_budget`](/docs/errors/smoke.random_budget) | error | A rulebook method drew too many random numbers |
| [`smoke.timeout`](/docs/errors/smoke.timeout) | error | A rulebook method did not return in time |
| [`smoke.cpu_budget`](/docs/errors/smoke.cpu_budget) | error | A rulebook method took longer than its CPU budget |
| [`smoke.worker_crashed`](/docs/errors/smoke.worker_crashed) | error | A rulebook method crashed the rule worker |
| [`smoke.call_failed`](/docs/errors/smoke.call_failed) | error | A rulebook method failed |

### Realtime conformance

| Code | Severity | Problem |
|---|---|---|
| [`conformance.settings`](/docs/errors/conformance.settings) | error | The realtime settings cannot be simulated |
| [`conformance.init_rejected`](/docs/errors/conformance.init_rejected) | error | rulebook.init rejected session creation |
| [`conformance.init_failed`](/docs/errors/conformance.init_failed) | error | rulebook.init failed in the simulated match |
| [`conformance.room_failed`](/docs/errors/conformance.room_failed) | error | The simulated room could not start |
| [`conformance.join_policy`](/docs/errors/conformance.join_policy) | error | Nobody can join under this joinPolicy |
| [`conformance.join_rejected`](/docs/errors/conformance.join_rejected) | error | A player could not join the simulated match |
| [`conformance.start_rejected`](/docs/errors/conformance.start_rejected) | error | The host could not start the simulated match |
| [`conformance.intent_rejected`](/docs/errors/conformance.intent_rejected) | error | A smoke intent was rejected in the simulated match |
| [`conformance.intent_ended_match`](/docs/errors/conformance.intent_ended_match) | error | A smoke intent ended the match before any tick |
| [`conformance.tick_threw`](/docs/errors/conformance.tick_threw) | error | rulebook.tick threw an error |
| [`conformance.tick_timeout`](/docs/errors/conformance.tick_timeout) | error | rulebook.tick did not finish in time |
| [`conformance.tick_invalid_result`](/docs/errors/conformance.tick_invalid_result) | error | rulebook.tick returned a result the platform cannot use |
| [`conformance.state_not_json`](/docs/errors/conformance.state_not_json) | error | The state is not plain JSON |
| [`conformance.state_too_large`](/docs/errors/conformance.state_too_large) | error | The state is over its size limit |
| [`conformance.view_not_json`](/docs/errors/conformance.view_not_json) | error | A view is not plain JSON |
| [`conformance.view_too_large`](/docs/errors/conformance.view_too_large) | error | A view is over its size limit |
| [`conformance.tick_too_slow`](/docs/errors/conformance.tick_too_slow) | error | rulebook.tick is over the hard time limit |
| [`conformance.checkpoint_failed`](/docs/errors/conformance.checkpoint_failed) | error | The final state could not be stored |
| [`conformance.rule_error`](/docs/errors/conformance.rule_error) | error | A rulebook method failed in the simulated match |
| [`conformance.crashed`](/docs/errors/conformance.crashed) | error | The simulated match crashed |
| [`conformance.no_details`](/docs/errors/conformance.no_details) | error | The simulated match failed without details |
| [`conformance.tick_over_budget`](/docs/errors/conformance.tick_over_budget) | warning | rulebook.tick averages over its time budget |
| [`conformance.match_ended_early`](/docs/errors/conformance.match_ended_early) | warning | The match ended itself in under 2 seconds |
| [`conformance.intent_skipped`](/docs/errors/conformance.intent_skipped) | warning | A smoke intent was skipped in a smaller run |

### Performance budgets

| Code | Severity | Problem |
|---|---|---|
| [`performance.tick_cpu`](/docs/errors/performance.tick_cpu) | warning | Server tick CPU is over budget |
| [`performance.bandwidth`](/docs/errors/performance.bandwidth) | warning | Bandwidth per player is over budget |
| [`performance.view_size`](/docs/errors/performance.view_size) | warning | Views are over budget |
| [`performance.state_size`](/docs/errors/performance.state_size) | warning | State is over budget |
| [`performance.server_bundle`](/docs/errors/performance.server_bundle) | warning | The server bundle is over budget |
| [`performance.client_page`](/docs/errors/performance.client_page) | warning | client/index.html is over budget |
| [`performance.assets`](/docs/errors/performance.assets) | warning | The assets are over budget |
| [`performance.pixel_ratio`](/docs/errors/performance.pixel_ratio) | warning | The client renders at the full device pixel ratio |
| [`performance.shadow_map`](/docs/errors/performance.shadow_map) | warning | A shadow map is over budget |
| [`performance.three_objects`](/docs/errors/performance.three_objects) | warning | The client creates too many 3D objects |
| [`performance.adaptive_quality`](/docs/errors/performance.adaptive_quality) | warning | Adaptive quality is off |
| [`performance.budget`](/docs/errors/performance.budget) | warning | A performance budget is missed |

### The platform

| Code | Severity | Problem |
|---|---|---|
| [`platform.conformance_unavailable`](/docs/errors/platform.conformance_unavailable) | error | Realtime games cannot be verified on this build |
| [`platform.check_failed`](/docs/errors/platform.check_failed) | error | The check itself failed |

### Other problems

| Code | Severity | Problem |
|---|---|---|
| [`check.unclassified`](/docs/errors/check.unclassified) | error | A problem without a specific code |

