Skip to main content

Vector2

Type<Vector2>
Two f32 components. 8 bytes.

Vector3

Type<Vector3>
Three f32 components. 12 bytes.
f32 gives roughly 7 significant digits, so world coordinates keep sub-millimetre precision anywhere inside Roblox’s playable volume. The dynamic encoder would have used f64 and spent 24 bytes plus a tag for the same value.

CFrame

Type<CFrame>
Position, then rotation as an axis vector plus an angle, all f32. 28 bytes.
Lossy. Encoded with CFrame:ToAxisAngle() and rebuilt with CFrame.fromAxisAngle(axis, angle) + position.An axis-angle round trip will not reproduce the original rotation matrix bit-for-bit, and a CFrame with no rotation has a degenerate axis. Fine for replicating visual transforms, and unsuitable where you need exact equality or accumulate the result over many hops.
If you only need yaw, which is the common case for character orientation, send a T.Vector3() position and one angle instead of a full CFrame:
16 bytes rather than 40, and lossless for what you actually care about.

Color3

Type<Color3>
Three u8 components. 3 bytes.
Lossy. Each component is math.floor(c * 255) and read back with Color3.fromRGB.Colours that originated as 8-bit RGB survive exactly. Computed values are quantised and the floor biases downward, so a component of 0.5 comes back as 127/255, about 0.498. Do not compare received colours for equality.

Instance

Type<Instance>
A varint index into the packet’s instance side-array. 1 to 3 bytes.
Instances never enter the buffer. They are appended to the writer’s insts array and passed as the remote’s second argument, so Roblox performs the reference translation natively. Only the index is serialised, which means a deeply nested instance costs the same as a top-level one.Within one batch, repeat references to the same instance are deduplicated to a single array entry.
Read returns r.insts[idx], which is nil when the receiver cannot see the instance, whether because it was not replicated, is server-only, or was destroyed before the flush. Flux does not error and the argument count still matches, so your callback runs with a nil in that position.Always nil-check received instances:
Note also that T.Instance() cannot encode a deliberate nil. Wrap it in T.Optional for that.

Enum

Type<EnumItem>
A single u8 index into the enum’s item list. 1 byte.
Enum
required
The enum class, not an item: Enum.KeyCode, not Enum.KeyCode.Space.
The constructor snapshots enum:GetEnumItems() at build time and maps each item to its zero-based position, so lookups on both ends are a single table index.
This is the largest single saving in the namespace. The dynamic encoder writes an enum as its full string name, and "Enum.KeyCode.Space" is 19 bytes plus a length prefix and a tag. T.Enum() is one byte. If you send enums at any volume, schematise them.
The index is a u8, so this only works for enums with 256 or fewer items. Larger enums silently wrap: an item at position 300 truncates to 44 and decodes as the wrong item.Most Roblox enums are small, but Enum.KeyCode and Enum.Font are near or past the boundary depending on engine version. Check before relying on it:
Use T.Any(), which sends the string form, for anything larger.
The index is positional, so it depends on the order GetEnumItems returns. Both sides compute this at runtime from the same engine version, so a live client and server always agree. An index is not stable across engine versions, though, so never persist a T.Enum() byte to a DataStore.

Size comparison

What each of these costs dynamically against schematised:
The vector and CFrame constructors are already f32-based in both paths, so the schema only removes the tag byte. Enums are the outlier, and Float32 over an implicitly-f64 bare number is the other.

Next steps

Composites

Nesting these into optionals, arrays and structs.

Serialization

Every lossy conversion in one place.