Multiplayer and Networking in Godot

01 — Architecture Overview

Godot's networking stack is layered. At the bottom sit transport-level peers (ENetMultiplayerPeer, WebSocketMultiplayerPeer, WebRTCMultiplayerPeer). Above that is the MultiplayerAPI, which handles peer tracking, RPC dispatch, and replication. The SceneTree exposes multiplayer as the default API instance.

LayerClassResponsibility
TransportENetMultiplayerPeerReliable/unreliable UDP channels, peer management
TransportWebSocketMultiplayerPeerTCP-based, browser-compatible
TransportWebRTCMultiplayerPeerP2P via STUN/TURN, browser-native
APIMultiplayerAPIRPC routing, replication, peer signals
APISceneMultiplayerDefault MultiplayerAPI; adds scene-level replication

Each SubViewport can have its own MultiplayerAPI instance — useful for isolating game worlds on a single server process.

02 — Peer Setup & MultiplayerAPI

The server always holds peer ID 1. Clients receive a unique positive integer assigned by the server on connection.

Server

var peer = ENetMultiplayerPeer.new()
peer.create_server(7777, max_clients)   # port, max_clients
multiplayer.multiplayer_peer = peer

multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)

Client

var peer = ENetMultiplayerPeer.new()
peer.create_client("127.0.0.1", 7777)
multiplayer.multiplayer_peer = peer

multiplayer.connected_to_server.connect(_on_connected)
multiplayer.connection_failed.connect(_on_failed)

Useful signals

SignalFires onAvailable to
peer_connected(id)new peer joinsserver & clients
peer_disconnected(id)peer dropsserver & clients
connected_to_serverclient handshake OKclient only
connection_failedserver unreachableclient only
server_disconnectedserver closedclient only
Note multiplayer.get_unique_id() returns your local peer ID. multiplayer.is_server() is shorthand for get_unique_id() == 1.

03 — Remote Procedure Calls (RPC)

Functions annotated with @rpc can be called across the network. The annotation accepts up to four arguments in this order: mode, sync, transfer mode, channel.

# Syntax
@rpc(mode, sync, transfer_mode, channel)
func my_function():
    pass
ParameterOptionsDefault
modeany_peer, authorityauthority
synccall_local, call_remotecall_remote
transfer_modereliable, unreliable, unreliable_orderedreliable
channelinteger (ENet: 0–255)0

Calling RPCs

# Call on all peers
rpc("apply_damage", amount)

# Call on a specific peer by ID
rpc_id(peer_id, "apply_damage", amount)

Typical patterns

# Only the server can call this on all clients
@rpc("authority", "call_local", "reliable")
func sync_health(new_hp: int):
    health = new_hp
    _update_hud()

# Any client can send input to the server
@rpc("any_peer", "call_remote", "unreliable_ordered")
func send_input(dir: Vector2):
    if is_multiplayer_authority():
        _process_input(dir)
Warning With any_peer, any connected client can invoke the function. Always validate inputs server-side before applying state changes.

04 — Authority & Ownership

Every node has a multiplayer authority — the peer responsible for driving it. Default authority is 1 (server). Authority is not automatically inherited by children; set it explicitly.

# Server assigns authority to a spawned player node
func _on_peer_connected(id: int):
    var player = PLAYER_SCENE.instantiate()
    player.name = str(id)
    player.set_multiplayer_authority(id)
    $Players.add_child(player)
# Guard logic to run only on the authority peer
func _physics_process(delta):
    if not is_multiplayer_authority(): return
    # Only the owning client runs movement here
    velocity = _get_input_vector() * speed
    move_and_slide()

set_multiplayer_authority(id, recursive) — set recursive = true to propagate to all children in one call.

05 — SceneReplication

Godot 4 ships two nodes that automate synchronization: MultiplayerSpawner and MultiplayerSynchronizer. Both require SceneMultiplayer as the active API (the default).

MultiplayerSpawner

Tracks instantiation and deletion of nodes under a spawn path and replicates those events to all peers. Configure spawn_limit, auto_spawn_path, and add scenes to the spawnable list in the inspector or via code.

# Server spawns; clients receive automatically
func spawn_bullet(pos: Vector2):
    var b = BULLET_SCENE.instantiate()
    b.position = pos
    $Bullets.add_child(b)           # spawner is watching $Bullets

MultiplayerSynchronizer

Continuously replicates named properties from the authority to all other peers on a configurable interval. Add properties in the Replication editor panel or via add_property().

# Code-driven property registration (Godot 4.1+)
func _ready():
    var sync: MultiplayerSynchronizer = $Synchronizer
    sync.add_property(NodePath(".:position"))
    sync.add_property(NodePath(".:health"))
PropertyPurpose
replication_intervalSeconds between sync packets. 0 = every frame.
delta_intervalSeconds between delta (change-only) packets.
visibility_filtersCallbacks to restrict sync to a subset of peers.
public_visibilityIf false, only peers passing visibility filters receive updates.
Note MultiplayerSynchronizer respects authority: only the authority peer sends updates; all others receive them. Combining a per-player Synchronizer with set_multiplayer_authority(peer_id) achieves client-authoritative movement with minimal code.

06 — Lobby Pattern & Scene Management

A common pattern: keep lobby logic in an autoload singleton and use it as the coordination point before loading the game scene.

# Network.gd (AutoLoad singleton)
signal player_list_changed

var players: Dictionary = {}  # { id: data }

@rpc("any_peer", "call_local", "reliable")
func register_player(data: Dictionary):
    var id = multiplayer.get_remote_sender_id()
    players[id] = data
    player_list_changed.emit()

@rpc("authority", "call_local", "reliable")
func start_game():
    get_tree().change_scene_to_file("res://game.tscn")

get_remote_sender_id() returns the peer ID of whoever triggered the RPC on this machine — critical for validating who is sending what.


Scene change coordination

Use rpc("start_game") called by the server after all players have registered. Because the singleton persists across scene changes, the peer list survives the transition intact. Instantiate player nodes in the game scene's _ready() using the data already in Network.players.

07 — Transport Layers

ENet (default)

UDP-based. Supports reliable, unreliable, and ordered channels. Lowest latency option for LAN and internet games. Channels (0–255) let you separate traffic streams; e.g., channel 0 for state, channel 1 for chat.

var peer = ENetMultiplayerPeer.new()
peer.create_server(7777, 32, 4)  # port, max_peers, max_channels

WebSocket

TCP fallback; mandatory for HTML5 exports. Use WebSocketMultiplayerPeer. API is identical to ENet from the multiplayer layer's perspective.

var peer = WebSocketMultiplayerPeer.new()
peer.create_server(7777)               # ws://
# For TLS: peer.create_server(7777, "*", tls_options)

WebRTC

Peer-to-peer; no dedicated server required for data relay, but a signaling server is needed for peer discovery. Use the WebRTC GDExtension plugin. Suited for browser-to-browser games or scenarios where hosting cost is a concern.

TransportProtocolHTML5P2PLatency
ENetUDPLow
WebSocketTCPMedium
WebRTCUDP/DTLSLow–Medium

08 — Latency Compensation

Godot does not include built-in lag compensation. You implement it at the game layer.

Client-side prediction

Run authoritative movement logic locally on the client (authority check disabled or duplicated), then reconcile when the server state arrives. If the server position diverges beyond a threshold, snap or interpolate back.

func _physics_process(delta):
    var input = _get_input_vector()
    _apply_movement(input, delta)      # run locally always
    if is_multiplayer_authority():
        rpc("server_move", input)       # also send to server

Server reconciliation

@rpc("authority", "call_local", "unreliable")
func correct_position(server_pos: Vector2):
    if position.distance_to(server_pos) > CORRECTION_THRESHOLD:
        position = position.lerp(server_pos, 0.3)

Entity interpolation

For remote (non-authority) entities, buffer received state snapshots and render between the two most recent frames. Aim for a buffer of ~100ms. This trades a small fixed delay for smooth visuals instead of jittery teleporting.

Note unreliable_ordered is the right transfer mode for position updates — drop old packets, never reorder. reliable is for events (damage, death, pickups) that must arrive exactly once.

09 — Security Considerations

Warning Godot's any_peer RPC mode is open to every connected peer — including other clients in a client-server topology. A compromised or malicious client can call those functions freely. Design accordingly.