Skip to main content
By default Flux encodes each value with a one-byte type tag followed by its data, so the receiver can decode anything without prior agreement. A schema replaces that with a fixed, agreed-upon layout: no tags, and every number written at exactly the width you declare.

How a schema is applied

A schema is a positional array of type constructors. Entry n encodes argument n.
A schema is a positional list. schema[1] encodes the first argument, schema[2] the second, and so on. The number of arguments you pass to Fire has to match the number of entries in the schema. Passing more arguments than the schema describes will error when Flux reaches a nil entry, and passing fewer writes a shorter packet than the receiver expects to read.
Schemas apply to events only. A schema encodes the payloads of Fire, FireUnreliable, FireAll and FireAllUnreliable. Invoke requests and their responses always fall back to the dynamic WriteAny/ReadAny encoder, whatever schema you passed to Flux.Server or Flux.Client.
Both sides have to declare the same schema. Flux does not negotiate or validate layouts. The sender writes what its schema says and the receiver reads what its schema says. A mismatch does not raise a clear error; it silently misreads the buffer and produces garbage or a cursor overrun.
Because schemas are registered globally by event name, the cleanest way to keep the two sides in sync is to define the schema once in a shared module:

What you save

The dynamic encoder already narrows integers to the smallest width that fits, so a schema’s saving on small integers is mostly the tag byte per value. The bigger wins are elsewhere: forcing a Float32 where the encoder would have written an 8-byte f64, and T.Enum(), which sends a one-byte index instead of the full "Enum.KeyCode.Space" string.

When a schema is worth it

Replicating positions every frame is where per-packet bytes actually add up, and it is also where UnreliableRemoteEvent’s size limit bites. A schema with T.Float32() components is the single most effective change you can make.
The dynamic encoder writes every non-integer as an f64. T.Float32() halves that, and for world positions or normalised values the precision loss is irrelevant.
Dynamically encoded EnumItems are sent as their full string name. T.Enum() sends a single byte. If you send enums at any volume, schematise them.
A purchase confirmation fired twice a minute does not need a schema. The dynamic encoder is convenient, tolerates changing argument shapes, and costs you nothing measurable at that rate.

Composites

Schema entries nest, so you can describe structured payloads without falling back to the dynamic encoder.

Optional

Prefixes the value with a single boolean byte. This is the only correct way to send a value that may be nil under a schema.
A plain T.Instance() or T.UInt8() cannot encode nil. Passing nil to a non-optional entry will error inside buffer.write*. Wrap anything nullable in T.Optional.

Array

Writes a varint length followed by each element.

Struct

Encodes a dictionary with named fields in a declared order. Entries are { name, type } pairs.
Only the declared fields are transmitted. Extra keys on the table are ignored, and a missing key is written as nil, which will error for a non-optional field type. Field order is part of the wire format, so both sides have to declare the fields in the same sequence.

Nesting

Composites compose freely:

Escaping a schema

T.Any() uses the dynamic encoder for that one entry. Use it when a single argument’s shape genuinely varies but the rest of the payload is fixed.

Bounded strings and arrays

T.String(maxLen) and T.Array(t, maxLen) assert against the limit.
The bound is checked on write only, and it raises a Luau error rather than dropping the message. On the server, a Fire with an over-long string will throw in your own code; on the client, an over-long Fire throws before anything is sent. Never treat maxLen as inbound validation of untrusted client data. Always re-check length inside your server listener.

Next steps

Schema constructors

Every entry in the Flux.Types namespace.

Serialization

How the dynamic encoder decides what to write.