Dynamic
WriteAny and ReadAny. One tag byte per value describing its type, then the value.
Handles anything, and agrees on nothing in advance.Schematised
schema[i].Write and .Read. No tags, because both sides already know the layout.
Fixed shape, smaller payload.The schema path applies to event payloads only. Invoke requests, invoke responses, the
event name and the packet header always use the dynamic path or a fixed internal
encoding. See Invocations.
Integer narrowing
The dynamic encoder inspects every number and picks the smallest representation that holds it. This happens automatically, and it is the main reason untyped Flux traffic is already reasonably compact.Varints
Lengths and indices use a LEB128-style varint: seven bits of payload per byte, high bit set to continue.
This is what makes short strings cheap.
"Chat" costs one length byte plus four data
bytes, where a fixed 4-byte length prefix would have doubled the overhead.
Varints encode the length of every string and buffer, the element count of every array,
instance indices, and argument counts of 63 or more. They are unsigned only, so negative
values are never varint-encoded.
Tables
The dynamic encoder classifies a table before writing it.- Arrays
- Dictionaries
A table whose keys are exactly
1..n is written as a compact array: the tag, a varint
count, then each element.1..n.
Mixed tables lose data.
{ 1, 2, Name = "x" } fails the array test and takes the
dictionary path, which preserves everything. But { [1] = "a", [3] = "c" } also takes
the dictionary path and is reconstructed as a sparse table rather than an array.
Metatables, functions and threads are never transmitted.Instances
Instances are not serialised into the buffer at all.1
Append to the side array
On write, the instance is pushed onto the writer’s
insts array, or found in its
instIndex map if it is already present in this batch.2
Write the index
A varint index into that array goes into the buffer. One or two bytes, regardless of
how deep the instance sits in the tree.
3
Ride alongside the buffer
Finalize returns (buffer, instances) and the remote is fired with both. Roblox
translates the instance references natively.This is why instance references are cheap, and why the same instance mentioned by
several events in one frame costs one entry total. It is also why you get
nil on the
other side if the receiver cannot see the instance: the index resolves against an array
whose entry Roblox declined to replicate. Always nil-check received instances.Lossy conversions
Some datatypes are deliberately encoded narrowly. These are not round-trip exact.CFrame: position plus axis-angle, 7 f32 values, 28 bytes
CFrame: position plus axis-angle, 7 f32 values, 28 bytes
Written as
Position, then the rotation as an axis vector plus an angle, all f32.
Reconstructed with CFrame.fromAxisAngle(axis, angle) + pos.Precision is float-level, and an axis-angle round trip will not reproduce the original
matrix bit-for-bit. Fine for replication, unsuitable for anything requiring exact
equality.Color3: three u8 values, 3 bytes
Color3: three u8 values, 3 bytes
Each component is
math.floor(c * 255) and read back with Color3.fromRGB.Colours that did not originate from 8-bit values are quantised, and the floor biases
downward. Color3.new(1, 1, 1) survives, but a computed 0.5 becomes 127/255
rather than 0.5.Region3: loses rotation
Region3: loses rotation
Written as its
CFrame plus Size, but reconstructed with
Region3.new(pos - size/2, pos + size/2), which is axis-aligned.Any rotation on the original region is discarded. In practice Region3 is always
axis-aligned anyway, but do not rely on the CFrame surviving.EnumItem: full string name
EnumItem: full string name
The dynamic encoder writes
tostring(item), such as "Enum.KeyCode.Space", and
resolves it by splitting on .. Correct and version-tolerant, but 20 or more bytes for
one value.Both directions are memoised, so repeated sends of the same item skip the tostring
and the lookup. Use T.Enum() in a schema to get one byte instead.TweenInfo: three f64 values, two enums and a bool
TweenInfo: three f64 values, two enums and a bool
All six fields are transmitted, with the two enums using the string encoding above.
Exact, but around 50 bytes. Send the parameters you actually vary instead if this is
hot.
Buffer growth and pooling
4096 bytes
Every writer starts at 4 KB.
doubling
When a write would overflow, capacity doubles until it fits, and the used bytes are
copied into the new buffer.
exact copy
On flush, only
cursor bytes are copied into a right-sized buffer, so an oversized
writer never sends padding.512 objects, 64 KB ceiling
Writers and readers are returned to free lists holding up to 512 each. A writer that
grew past 64 KB is dropped rather than pooled, so one huge payload does not permanently
inflate memory.
Because the initial 4 KB comfortably holds a typical frame’s traffic, steady-state Flux
performs no buffer allocation at all. Writers cycle between the pool and the bridge, and
only
Finalize allocates, because the remote needs a buffer it owns.Next steps
Type tags
The full tag table and per-type byte costs.
Supported types
Everything Flux can and cannot send.
