> ## Documentation Index
> Fetch the complete documentation index at: https://flux-docs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Batching

> Every send accumulates into a shared buffer and flushes once per frame.

Flux never sends immediately. `Fire` appends bytes to a `Writer` and returns, and the
`Bridge` flushes accumulated writers on `RunService.Heartbeat`.

## Flush timing

```luau theme={null}
local FRAME_TIME = 1 / 60

RunService.Heartbeat:Connect(function(dt)
	accumulator += dt
	if accumulator >= FRAME_TIME then
		local flushed = false
		while accumulator >= FRAME_TIME do
			accumulator -= FRAME_TIME
			flushed = true
		end
		if flushed then Bridge.FlushAll() end
	end
end)
```

The accumulator drains in `FRAME_TIME` steps, but `FlushAll` is called at most once per
`Heartbeat`. A frame that overshoots, say a 50 ms hitch, drains three steps and still
performs a single flush, so the queue never backs up into a burst of sends.

<Info>
  The effective cadence is therefore `min(heartbeat rate, 60Hz)`. At a healthy 60 FPS
  that is one flush per frame. On a client rendering at 30 FPS it is one flush per frame
  at 30 Hz, because Flux does not send faster than the heartbeat that drives it.
</Info>

## Writer groups

Sends are grouped by recipient and channel, because each group becomes one remote call.

<Tabs>
  <Tab title="Server">
    Four groups:

    | Group                  | Populated by                  | Flushed with                     |
    | ---------------------- | ----------------------------- | -------------------------------- |
    | Reliable, per-player   | `Fire(player, ...)`           | `Flux_Reliable:FireClient`       |
    | Unreliable, per-player | `FireUnreliable(player, ...)` | `Flux_Unreliable:FireClient`     |
    | Reliable, broadcast    | `FireAll(...)`                | `Flux_Reliable:FireAllClients`   |
    | Unreliable, broadcast  | `FireAllUnreliable(...)`      | `Flux_Unreliable:FireAllClients` |

    The per-player groups are maps keyed by `Player`, so a frame with sends to twelve
    different players produces twelve `FireClient` calls, one each, however many events
    were involved.
  </Tab>

  <Tab title="Client">
    Two groups, both destined for the server:

    | Group      | Populated by               | Flushed with                 |
    | ---------- | -------------------------- | ---------------------------- |
    | Reliable   | `Fire(...)`, `Invoke(...)` | `Flux_Reliable:FireServer`   |
    | Unreliable | `FireUnreliable(...)`      | `Flux_Unreliable:FireServer` |

    The client keys its writers under the constant `"SERVER"`, since there is only ever
    one recipient.
  </Tab>
</Tabs>

<Note>
  Broadcasts are flushed before per-player buffers. Across the two, ordering is not
  guaranteed: a `FireAll` issued after a `Fire(player, ...)` in the same frame will arrive
  at that player first, because it belongs to a different buffer. Ordering is only
  guaranteed within a single writer group.
</Note>

## What batching buys

<CardGroup cols={2}>
  <Card title="One remote call, many events" icon="compress" horizontal>
    Twenty `Fire` calls to one player in a frame become one `FireClient` with one buffer,
    instead of twenty separate network events.
  </Card>

  <Card title="Amortised per-call overhead" icon="gauge-high" horizontal>
    Roblox's per-remote-invocation cost is paid once per group per frame rather than once
    per message.
  </Card>

  <Card title="Reused allocations" icon="recycle" horizontal>
    Writers come from a pool of up to 512 and are returned after each flush, so steady
    traffic stops allocating.
  </Card>

  <Card title="Shared instance table" icon="link" horizontal>
    Instances referenced by several events in one batch are deduplicated into a single
    side array.
  </Card>
</CardGroup>

### Deduplicated instances

Within one writer, an `Instance` is only appended once. Repeat references write a varint
index to the existing entry.

```luau theme={null}
-- All three reference the same humanoid; the batch carries it once.
Damage:Fire(player, humanoid, 10)
Damage:Fire(player, humanoid, 15)
Effect:Fire(player, humanoid, "sparks")
```

<Info>
  Instances never enter the buffer at all. They ride in the `{Instance}` array that
  `FireClient` takes as its second argument, and Roblox handles their reference
  translation. Only the index is serialised. This is also why sending an instance the
  receiver cannot see yields `nil` rather than an error.
</Info>

## Latency cost

<Warning>
  Batching adds up to one frame of delay, roughly 16 ms at 60 FPS, between `Fire`
  returning and the bytes leaving. This is on top of normal network latency.

  For almost all gameplay this is invisible and well worth the throughput. It does mean
  Flux is not the right tool if you need a message on the wire this instant, and that a
  `Fire` immediately followed by a `Destroy` of the referenced instance may flush after
  the instance is gone.
</Warning>

There is no public flush API. `Bridge.FlushAll()` exists but the `Bridge` module is
internal and not re-exported from the root, so reaching into it to force a send is
unsupported and will fight the accumulator.

## Ordering rules, summarised

<AccordionGroup>
  <Accordion title="Guaranteed" icon="check">
    Two reliable sends to the same recipient in the same frame arrive in the order you
    made them, because they are adjacent in one buffer and the receiver decodes
    sequentially.

    This holds across different event names, since all events share the writer.
  </Accordion>

  <Accordion title="Not guaranteed" icon="xmark">
    * Reliable against unreliable, which use separate remotes and separate buffers.
    * `FireAll` against `Fire`, which use separate writer groups, and broadcasts flush
      first.
    * Anything unreliable against anything else, since those packets may be dropped or
      reordered in transit by definition.
    * Listener execution order for a single packet, because each callback is
      `task.spawn`ed and the list is walked backwards.
  </Accordion>
</AccordionGroup>

## Player disconnects

When a player leaves, the server frees both of their pending writers and drops the map
entries in `Players.PlayerRemoving`. Anything written to them that frame but not yet
flushed is discarded.

<Note>
  `Fire(player, ...)` on a player who has already left creates a fresh writer for a
  `Player` object that no longer has a map entry, and `FireClient` on a departed player
  does nothing. It will not error, but it does briefly hold a reference, so check
  `player.Parent == Players` before firing in long-lived loops.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Wire format" icon="binary" href="/concepts/wire-format/packets">
    What a flushed buffer actually contains.
  </Card>

  <Card title="Bridge API" icon="layer-group" href="/api/internals/bridge">
    The batching module in full.
  </Card>
</CardGroup>
