State updates
When your game changes entity fields during a tick, golem serializes those changes and sends them to connected clients automatically. The default state-update lane is WebTransport datagrams, which lets newer movement and gameplay state keep flowing even when an older update is lost.
srv := golem.NewServer(golem.ServerConfig{ Addr: ":4433", DevSelfSignedCert: true, // local development only})The default server transport is WebTransport, and the default state-update lane is StateUpdateLaneDatagram. You only need to set StateUpdateLane when you want to force the stream path instead:
srv := golem.NewServer(golem.ServerConfig{ Addr: ":4433", StateUpdateLane: golem.StateUpdateLaneStream,})Choosing a state-update lane
Section titled “Choosing a state-update lane”Use StateUpdateLaneDatagram for almost all games. It sends incremental entity state over WebTransport datagrams and tracks which state the client has acknowledged. If a datagram disappears on an unstable connection, later updates are not stuck waiting behind it; golem rebases lost fields onto newer values and keeps sending current state. In practice, that avoids head-of-line blocking for fast-changing state like positions, animation, health, or AI state.
Use StateUpdateLaneStream when the client transport has no datagram lane, such as WebSocket or a custom client that only reads reliable stream frames. The stream can also work better under an unusually dense scene, such as hundreds of players and monsters inside one small map, because it carries large ordered batches with transport backpressure instead of trying to fit a very large amount of per-tick state into small datagram payloads.
Both lanes keep the same client-facing update protocol. Generated JS, Go, and C# clients still apply spawns, deltas, removals, and world data through the generated managers.
The stream lane always carries:
- world snapshot data
- entity snapshots sent when a client connects
- full state for entities entering a player’s area of interest
- spawn frames
- removal frames
- server events
On StateUpdateLaneStream, incremental delta frames also use the stream. On StateUpdateLaneDatagram, incremental state normally uses datagrams, while full-state payloads and oversized deltas fall back to the stream.
Size and batching
Section titled “Size and batching”golem builds stream updates as ordered logical frames first, then lets the active reliable transport coalesce those frames into efficient writes. A single wrapped stream update that is already larger than 32000 bytes is treated as an error instead of being fragmented mid-message.
On the WebSocket path, which requires StateUpdateLaneStream, a large tick is split across multiple ordered binary WebSocket messages targeting 32000 bytes each. The client still sees the same ordered sequence of wrapped ServerMessage blobs.
On the WebTransport stream path, a large tick is split across multiple ordered writes on the reliable stream using the same 32000-byte chunk target. Each logical frame remains length-prefixed inside the stream, so transport write boundaries can differ from WebSocket even though decoded ordering stays the same.
Client behavior
Section titled “Client behavior”The generated JS, Go, and C# clients do not need a separate state-update API. createClient() in JavaScript, CreateClient() in Go, and ClientFactory.CreateClient() in C# unwrap reliable ServerMessage frames and feed every EntityUpdate into the generated EntityManager. On WebTransport, they also apply compact datagram state automatically.
Per-tick updates
Section titled “Per-tick updates”When Addr is set in your ServerConfig, the server broadcasts all entity changes to connected clients automatically after each tick. No explicit Broadcast call is needed.
If you want to inspect or instrument the updates before they go out, register an OnUpdates hook:
srv.OnUpdates(func(updates [][]byte) { metrics.RecordTick(len(updates))})OnUpdates runs before auto-broadcast, so you can log, count, or filter without affecting delivery.
What’s inside each update
Section titled “What’s inside each update”Each blob encodes one EntityUpdate using your generated serializers, wrapped in a ServerMessage envelope on the wire. The GameClient unwraps the envelope transparently—generated code and entity callbacks see EntityUpdate payloads as before. There are three kinds:
- Spawns — full state for entities added this tick, so clients can create them from scratch.
- Deltas — only the fields that changed for entities that already exist. Fields declared
sync: tickin your YAML participate in deltas;sync: oncefields are sent only on spawn. - Removals — a lightweight message containing the entity ID, produced by your
MarshalEntityRemovedhook.
Generated state, delta, and removal payloads also include a state revision used by the generated JS manager to reject stale entity updates.
On the client, createClient() wires decoding automatically for JavaScript, CreateClient() does the same for generated Go clients, and ClientFactory.CreateClient() does the same for generated C# clients. See JavaScript client, Go client, and Unity client.
World data updates
Section titled “World data updates”If your project uses world data schemas, stored world data is sent to each client on connect (before entity snapshots) and whenever the server calls PushWorldData. World updates arrive as ServerMessage frames with a world_update payload (tag 2), separate from entity updates (tag 1). On the JS client, createClient() wires a WorldManager that dispatches per-type callbacks automatically.
Bandwidth considerations
Section titled “Bandwidth considerations”The default path broadcasts every update to every connected client each tick. For most games this is the right starting point.
When your world is larger or player count grows, enable interest management by setting CellSize in ServerConfig. Each client then receives only entities within its field of interest—spawns, deltas, and removals are all filtered per session with no client-side changes needed.
Collection fields in deltas
Section titled “Collection fields in deltas”Entity vars typed as list<T> or dict<K,V> follow replace-whole delta semantics. On any tick where a collection field is dirty, the complete current value of that field is sent—not individual element diffs.
Clients see one of two states per delta for a collection field:
- Absent — the field is unchanged; the client keeps what it has.
- Present — the field has a new authoritative value, which may be empty (meaning the collection was explicitly cleared).
This applies symmetrically in both the Go server structs and the generated TypeScript classes. On the Go side, nil signals absent and a non-nil empty slice/map signals cleared. On the TypeScript side, undefined signals absent and an empty array/object signals cleared.