Skip to main content
Flux is seven modules. Three are public-facing and four are internal.

The modules

A thin façade. It exports the Server<Args..., Out...> and Client<Args..., Out...> types, re-exports the Types namespace, and guards each constructor against being called in the wrong context. It holds no state.See Flux.
Owns the server’s listener table, invoke-handler table and schema table. All three are keyed by event name and all are module-level, so they are shared by every Server object with the same name. Installs the OnServerEvent handlers on first use and dispatches decoded packets.See Server.
The mirror of Server, plus the pending-request table that makes Invoke work: an incrementing request id per name, a parked thread, and a timeout timer.See Client.
Hands out the Writer a send should append to, and flushes accumulated writers on Heartbeat. Keeps four groups of writers on the server (reliable per-player, unreliable per-player, reliable broadcast, unreliable broadcast) and two on the client.See Bridge.
All wire I/O. Pooled Writer and Reader objects, primitive read and write pairs, Roblox datatype pairs, the varint codec, the packet header codec, and the dynamic WriteAny/ReadAny encoder. Compiled --!native --!optimize 2.See Buffers.
A namespace of factories that each return a { Write, Read } pair. Nothing more, so a schema is just an array of these.See Types.
Finds or creates the Flux_Reliable and Flux_Unreliable instances and caches them. The server creates, the client waits.See Remotes.

Two remotes, not two hundred

The central design decision: Flux creates exactly one RemoteEvent and one UnreliableRemoteEvent for the entire game, no matter how many events you declare.
Each instance replicates, each call is its own network event, and each carries its own per-call overhead.

Following a single Fire

Here is the whole path for Damage:Fire(player, humanoid, 250).
1

Resolve a writer

Server:Fire calls Bridge.Writer(true, player). The bridge looks up its reliable per-player map and returns the existing Writer for that player, or takes a fresh one from the pool.Nothing is sent yet.
2

Write the channel name

Buffers.WriteString(w, "Damage") writes a varint length followed by the raw bytes. This is how the receiver knows which listeners to run.
3

Write the packet header

One byte: the two-bit packet type (1 for an event) in the high bits, the argument count in the low six. Counts of 63 or more escape to a trailing varint.
4

Write the arguments

With a schema, schema[i].Write(w, arg) for each argument, with no type tags. Without one, Buffers.WriteAny(w, arg), which writes a tag byte then the value, narrowing integers to the smallest width that fits.Instances are the exception. They are appended to the writer’s side array and only their index goes into the buffer.
5

Accumulate

Fire returns. Any further sends to the same player on the reliable channel, from any event, append to this same writer, one after another.
6

Flush on Heartbeat

Once at least 1/60s of accumulated time has passed, Bridge.FlushAll finalises each writer: it copies the used bytes into an exactly-sized buffer, fires Flux_Reliable:FireClient(player, buffer, instances), and returns the writer to the pool.
7

Decode on the client

Client’s OnClientEvent handler wraps the buffer in a pooled Reader and loops while r.cursor < r.len, reading a name, a header and that many arguments each pass, so every packet in the batch is handled.
8

Dispatch

For each decoded packet, listeners registered under that name are invoked with task.spawn, iterating the list from the end backwards. Once and Wait entries remove themselves. The reader goes back to the pool.

State lives in the module, not the object

Every table that matters is module-level and keyed by event name.
This is why two Flux.Server("Chat") calls behave as one channel. The objects are separate tables, but every lookup they perform goes through self._id into the shared module state. The object is little more than a typed handle carrying a name and a schema.
It is also why a name collision between two unrelated systems silently crosses their traffic, and why assigning OnInvoke twice on the same name replaces rather than adds. Namespace your event names in a large place.

Next steps

Batching

Flush timing, and what it means for latency.

Serialization

How values become bytes.