Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when building dedicated servers — headless export, server architecture, lobby management, and deployment
.claude/skills/jame581-dedicated-server/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 112% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 66% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 193% | 0% |
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, C# follows.
Related skills: See multiplayer-basics for ENet setup, RPCs, and authority model. See multiplayer-sync for state synchronization and interpolation.
A dedicated server runs without a display, GPU, or audio device. Godot supports this through the --headless flag and a dedicated export preset.
Pass --headless on the command line to suppress the display and audio drivers at runtime:
./my_game.x86_64 --headlessThis is distinct from the server platform — --headless is a runtime flag that works on any exported binary. The server export template strips rendering entirely from the binary, reducing its size.
In the Godot editor, create a dedicated Linux/X11 (or Linux Server) export preset:
Linux Server.Use OS.has_feature() to branch between server and client code at runtime. Define a custom server feature in the export preset (Project Settings → Export → Custom Features) or rely on the built-in dedicated_server feature that the server template sets automatically:
gdscript# boot.gd — autoload, runs before any scene loads extends Node func _ready() -> void: if OS.has_feature("dedicated_server") or DisplayServer.get_name() == "headless": # Disable rendering-dependent systems RenderingServer.set_render_loop_enabled(false) # Start server logic ServerBootstrap.start() else: # Start client logic ClientBootstrap.start()
csharp// Boot.cs — autoload, runs before any scene loads. using Godot; public partial class Boot : Node { public override void _Ready() { if (OS.HasFeature("dedicated_server") || DisplayServer.GetName() == "headless") { // Disable the render loop. The window is invisible but the engine still ticks. RenderingServer.SetRenderLoopEnabled(false); ServerBootstrap.Start(); } else { ClientBootstrap.Start(); } } }
> Note: the export preset configuration (custom features, exclude list, "Export As Dedicated Server" flag) is identical regardless of language — see the GDScript section above for preset settings.
Feature tag summary:
| Tag | Set by | Notes | |-----|--------|-------| | dedicated_server | Server export template | Most reliable way to detect a server binary | | headless | --headless CLI flag | Set at runtime, not baked into the binary | | Custom server | Your export preset's Custom Features | Useful when sharing a binary between roles |
On a headless server, _process and _physics_process still run normally — but nothing is rendered. Keep all server logic in _physics_process for deterministic, fixed-rate updates.
gdscript# server_main.gd — add as autoload named ServerMain extends Node ## Physics frames per second — matches Project Settings → Physics → Common → Physics Ticks Per Second. ## Override via --tick-rate CLI argument (see Section 5). var tick_rate: int = 60 ## Current server tick counter. var server_tick: int = 0 func _ready() -> void: # Guard: this node does nothing on the client. if not _is_server(): set_process(false) set_physics_process(false) return Engine.physics_ticks_per_second = tick_rate print("[Server] Started — tick rate: %d Hz" % tick_rate) func _physics_process(_delta: float) -> void: server_tick += 1 _tick_game_logic() func _tick_game_logic() -> void: # All authoritative game simulation goes here. # Never reference Camera, CanvasLayer, or any rendering node from this path. pass ## Returns true when this process is acting as the authoritative server. func _is_server() -> bool: # Covers both: dedicated binary and hosted listen-server. return multiplayer.is_server()
Structure your scenes so server-only nodes are in a dedicated branch and skipped on clients:
gdscript# world.gd extends Node @onready var server_systems: Node = $ServerSystems # physics, AI, scoring @onready var client_systems: Node = $ClientSystems # camera, HUD, audio func _ready() -> void: # Disable server systems on clients and vice versa. server_systems.set_process_mode( PROCESS_MODE_ALWAYS if multiplayer.is_server() else PROCESS_MODE_DISABLED ) client_systems.set_process_mode( PROCESS_MODE_DISABLED if multiplayer.is_server() else PROCESS_MODE_ALWAYS )
Use these guards at the top of scripts that must behave differently in the editor, on the server, and on clients:
gdscriptfunc _ready() -> void: if Engine.is_editor_hint(): return # Skip all runtime setup in editor preview if multiplayer.is_server(): _server_init() else: _client_init() func _server_init() -> void: print("[Server] Initializing authoritative state") func _client_init() -> void: print("[Client] Initializing local presentation layer")
csharp// ServerMain.cs — add as autoload named ServerMain using Godot; public partial class ServerMain : Node { /// <summary>Physics ticks per second. Override via --tick-rate CLI argument.</summary> public int TickRate { get; set; } = 60; /// <summary>Current server tick counter.</summary> public int ServerTick { get; private set; } public override void _Ready() { if (!IsServer()) { SetProcess(false); SetPhysicsProcess(false); return; } Engine.PhysicsTicksPerSecond = TickRate; GD.Print($"[Server] Started — tick rate: {TickRate} Hz"); } public override void _PhysicsProcess(double delta) { ServerTick++; TickGameLogic(); } private void TickGameLogic() { // All authoritative game simulation goes here. } private bool IsServer() => Multiplayer.IsServer(); }
csharp// World.cs using Godot; public partial class World : Node { [Export] private Node _serverSystems = null!; [Export] private Node _clientSystems = null!; public override void _Ready() { if (Engine.IsEditorHint()) return; _serverSystems.ProcessMode = Multiplayer.IsServer() ? ProcessModeEnum.Always : ProcessModeEnum.Disabled; _clientSystems.ProcessMode = Multiplayer.IsServer() ? ProcessModeEnum.Disabled : ProcessModeEnum.Always; } }
dedicated_server feature is set automatically)OS.has_feature("dedicated_server") or DisplayServer.get_name() == "headless" to branch server vs client startupRenderingServer.set_render_loop_enabled(false) called on the server to prevent any render workPROCESS_MODE_DISABLED on clients; client-only nodes use PROCESS_MODE_DISABLED on the serverEngine.is_editor_hint() guard at the top of _ready() in every script that has side effectsServerConfig parses --port, --max-players, --tick-rate from CLI args before _ready() of other autoloadsserver.cfg does not existSERVER_PORT, SERVER_MAX_PLAYERS, SERVER_TICK_RATE) are applied after config file, before CLI argsLobbyManager.player_list size is checked against max_players before accepting a new peerMatchState returns to LOBBY so players must re-confirm each roundMatchManager only runs _physics_process on the server (SetPhysicsProcess(false) on clients)_physics_process delta, not Timer nodes (avoids scene dependency).pck file to the imageRestart=on-failure so the server recovers from crashes automaticallyjournalctl -u <service> -fA per-player state dictionary keyed by peer_id is the canonical pattern. Server holds the authoritative dict; clients receive updates via RPC. Implement a --max-players CLI cap and a ready-toggle RPC so all peers can confirm before starting.
> See references/lobby-management.md for the full GDScript and C# lobby implementation (player_list dict, max-players cap, ready toggle RPC, broadcast pattern).
Drive lobby → countdown → in-game → results with a state machine. Server is authoritative — clients only receive state-change RPCs. Common states: LOBBY, COUNTDOWN, IN_GAME, RESULTS.
> See references/match-flow.md for the full state machine with countdown/results timers and GDScript + C# implementations.
Parse CLI flags from OS.get_cmdline_args() for --port, --max-players, --tick-rate, --log-level. Pre-set Engine.physics_ticks_per_second before the first physics frame; reading and writing the others is straightforward match / switch work.
> See references/server-config.md for the GDScript and C# argument-parsing helper that reads all four flags safely at startup.
A Linux VPS with a Dockerfile and systemd service file is the standard production layout. The Dockerfile bundles the headless export template, the exported PCK, and the .NET runtime (for C# projects). systemd handles auto-restart, log rotation via journald, and resource limits.
> See references/deployment.md for the Dockerfile, the Linux VPS setup steps, the systemd unit file, and log-rotation configuration.
dedicated_server feature tag addedRenderingServer.set_render_loop_enabled(false) (and audio bus muted) when OS.has_feature("dedicated_server") is truemultiplayer.is_server() before initializing systems| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 15,748 | 13,563 | -14% | 1 | 1 | 0% | 2,755 | 5,395 | +96% | 0 | 0 | — |
case-02 | pass→pass | 16,689 | 12,843 | -23% | 1 | 1 | 0% | 3,036 | 5,273 | +74% | 0 | 0 | — |
case-03 | fail→pass | 16,202 | 9,239 | -43% | 1 | 1 | 0% | 2,865 | 4,643 | +62% | 0 | 0 | — |
case-04 | fail→pass | 15,613 | 6,871 | -56% | 1 | 1 | 0% | 2,920 | 4,246 | +45% | 0 | 0 | — |
case-05 | pass→pass | 12,541 | 10,738 | -14% | 1 | 1 | 0% | 2,217 | 4,898 | +121% | 0 | 0 | — |
case-06 | fail→pass | 14,132 | 11,101 | -21% | 1 | 1 | 0% | 2,199 | 4,669 | +112% | 0 | 0 | — |
case-07 | pass→pass | 9,189 | 2,520 | -73% | 1 | 1 | 0% | 1,498 | 3,294 | +120% | 0 | 0 | — |
case-08 | pass→pass | 8,669 | 7,456 | -14% | 1 | 1 | 0% | 1,519 | 4,192 | +176% | 0 | 0 | — |
case-09 | pass→pass | 4,314 | 3,641 | -16% | 1 | 1 | 0% | 724 | 3,522 | +386% | 0 | 0 | — |
case-10 | pass→pass | 13,386 | 7,836 | -41% | 1 | 1 | 0% | 2,568 | 4,393 | +71% | 0 | 0 | — |
case-11 | fail→fail | 13,310 | 6,981 | -48% | 1 | 1 | 0% | 2,349 | 4,283 | +82% | 0 | 0 | — |
case-12 | fail→pass | 16,885 | 9,106 | -46% | 1 | 1 | 0% | 2,592 | 4,290 | +66% | 0 | 0 | — |
case-13 | fail→pass | 8,114 | 5,763 | -29% | 1 | 1 | 0% | 1,352 | 3,960 | +193% | 0 | 0 | — |
case-14 | pass→pass | 17,636 | 11,052 | -37% | 1 | 1 | 0% | 3,001 | 4,731 | +58% | 0 | 0 | — |
case-15 | pass→pass | 12,665 | 5,501 | -57% | 1 | 1 | 0% | 2,102 | 3,830 | +82% | 0 | 0 | — |
case-16 | pass→pass | 7,930 | 7,890 | -1% | 1 | 1 | 0% | 1,156 | 4,059 | +251% | 0 | 0 | — |
case-17 | pass→pass | 19,558 | 6,793 | -65% | 1 | 1 | 0% | 2,855 | 3,853 | +35% | 0 | 0 | — |
case-18 | pass→pass | 15,542 | 9,075 | -42% | 1 | 1 | 0% | 2,238 | 4,202 | +88% | 0 | 0 | — |
case-19 | fail→pass | 11,049 | 3,954 | -64% | 1 | 1 | 0% | 2,011 | 3,670 | +82% | 0 | 0 | — |
case-20 | pass→pass | 9,095 | 9,270 | +2% | 1 | 1 | 0% | 1,637 | 4,630 | +183% | 0 | 0 | — |
case-21 | pass→pass | 12,313 | 8,318 | -32% | 1 | 1 | 0% | 2,140 | 4,273 | +100% | 0 | 0 | — |
case-22 | pass→pass | 13,588 | 13,171 | -3% | 1 | 1 | 0% | 2,136 | 5,253 | +146% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.