Skip to main content
An event is a named channel. Both sides construct it independently with the same name, and every send and listen pair on that name lines up.

Sending

The four send methods differ along two axes: who receives the message, and whether delivery is guaranteed.
Sends are asynchronous and do not block. Fire writes into a buffer and returns immediately; the buffer is flushed on the next Heartbeat. See Batching for the exact timing.

Choosing reliable or unreliable

Damage, currency, inventory changes, round transitions, UI state. If dropping the message would leave the two sides disagreeing, it has to be reliable.
Positions, aim direction, cosmetic effects, anything you re-send every frame. A dropped packet is corrected by the next one, so the loss costs nothing.
Reliable and unreliable travel over separate remotes and are flushed into separate buffers. A FireUnreliable issued before a Fire may arrive after it. Do not split one logical message across both channels.

Listening

Connect

Connect registers a persistent listener and returns a handle with a single Disconnect method.
On the server, the first parameter is always the sending Player, followed by the arguments. On the client there is no player parameter. This is the most common source of off-by-one argument bugs when moving code between contexts.

Once

Once fires at most one time, then removes itself.
Once returns nothing. There is no handle, so a Once listener cannot be cancelled before it fires. If you need to be able to back out, use Connect and disconnect inside the callback.

Wait

Wait yields the calling thread until the next message arrives, then returns the arguments directly.
Wait has to be called from a yieldable thread, because it uses coroutine.running() and coroutine.yield() internally. Calling it from a context that cannot yield will error. It also has no timeout: if the message never arrives, the thread stays parked forever. Guard long waits with task.delay yourself.

Multiple listeners

Any number of listeners can be attached to the same name, and each will receive every message.
Ordering is not registration order. Flux dispatches listeners from the end of its internal list backwards, so the most recently connected listener runs first. Each callback is also spawned with task.spawn, which means they run concurrently and one erroring listener never blocks the others. Do not write code that depends on listener order.

Listeners are keyed by name, not by object

Constructing the same name twice does not give you two isolated channels. Both objects read and write the same internal listener table.
This makes it safe to call Flux.Server("Name") in several modules rather than passing one object around. It also means a name collision between two unrelated systems will silently cross their traffic. Prefix your names once a place gets large: "Combat.Hit", "Shop.Purchase".

Next steps

Invocations

Round trips with return values.

Connections

Disconnecting, and what Flux does not clean up for you.