Fire appends bytes to a Writer and returns, and the
Bridge flushes accumulated writers on RunService.Heartbeat.
Flush timing
FRAME_TIME steps, but FlushAll is called at most once per
Heartbeat. A frame that overshoots, say a 50 ms hitch, drains three steps and still
performs a single flush, so the queue never backs up into a burst of sends.
The effective cadence is therefore
min(heartbeat rate, 60Hz). At a healthy 60 FPS
that is one flush per frame. On a client rendering at 30 FPS it is one flush per frame
at 30 Hz, because Flux does not send faster than the heartbeat that drives it.Writer groups
Sends are grouped by recipient and channel, because each group becomes one remote call.- Server
- Client
Four groups:
The per-player groups are maps keyed by
Player, so a frame with sends to twelve
different players produces twelve FireClient calls, one each, however many events
were involved.Broadcasts are flushed before per-player buffers. Across the two, ordering is not
guaranteed: a
FireAll issued after a Fire(player, ...) in the same frame will arrive
at that player first, because it belongs to a different buffer. Ordering is only
guaranteed within a single writer group.What batching buys
One remote call, many events
Twenty
Fire calls to one player in a frame become one FireClient with one buffer,
instead of twenty separate network events.Amortised per-call overhead
Roblox’s per-remote-invocation cost is paid once per group per frame rather than once
per message.
Reused allocations
Writers come from a pool of up to 512 and are returned after each flush, so steady
traffic stops allocating.
Shared instance table
Instances referenced by several events in one batch are deduplicated into a single
side array.
Deduplicated instances
Within one writer, anInstance is only appended once. Repeat references write a varint
index to the existing entry.
Instances never enter the buffer at all. They ride in the
{Instance} array that
FireClient takes as its second argument, and Roblox handles their reference
translation. Only the index is serialised. This is also why sending an instance the
receiver cannot see yields nil rather than an error.Latency cost
There is no public flush API.Bridge.FlushAll() exists but the Bridge module is
internal and not re-exported from the root, so reaching into it to force a send is
unsupported and will fight the accumulator.
Ordering rules, summarised
Guaranteed
Guaranteed
Two reliable sends to the same recipient in the same frame arrive in the order you
made them, because they are adjacent in one buffer and the receiver decodes
sequentially.This holds across different event names, since all events share the writer.
Not guaranteed
Not guaranteed
- Reliable against unreliable, which use separate remotes and separate buffers.
FireAllagainstFire, which use separate writer groups, and broadcasts flush first.- Anything unreliable against anything else, since those packets may be dropped or reordered in transit by definition.
- Listener execution order for a single packet, because each callback is
task.spawned and the list is walked backwards.
Player disconnects
When a player leaves, the server frees both of their pending writers and drops the map entries inPlayers.PlayerRemoving. Anything written to them that frame but not yet
flushed is discarded.
Fire(player, ...) on a player who has already left creates a fresh writer for a
Player object that no longer has a map entry, and FireClient on a departed player
does nothing. It will not error, but it does briefly hold a reference, so check
player.Parent == Players before firing in long-lived loops.Next steps
Wire format
What a flushed buffer actually contains.
Bridge API
The batching module in full.
