> ## 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.

# Packets

> The framing Flux uses inside every buffer it sends.

A flushed buffer contains one or more packets laid end to end. There is no outer header
and no packet count, because the receiver simply loops until the cursor reaches the end
of the buffer.

```luau theme={null}
while r.cursor < r.len do
	local id = Buffers.ReadString(r)
	local ptype, argCount = Buffers.ReadPacketHeader(r)
	-- read argCount arguments, dispatch, repeat
end
```

## Packet layout

Every packet has the same three-part shape, read in this order:

| Order | Segment      | Encoding                                   | Size                       |
| ----- | ------------ | ------------------------------------------ | -------------------------- |
| 1     | Channel name | varint length, then raw bytes              | 1 to 3 bytes plus the name |
| 2     | Header       | packet type and argument count, bit-packed | 1 byte, or 1 plus a varint |
| 3     | Payload      | schema, or one tag and value per argument  | varies                     |

<ResponseField name="Channel name" type="varint-prefixed string">
  The event name you passed to `Flux.Server` or `Flux.Client`, written with `WriteString`.
  This is what the dispatcher keys listener lookup on, so it is repeated in every packet.
  Keep names short if you send at very high frequency.
</ResponseField>

<ResponseField name="Header" type="1 byte, or 1 plus a varint">
  A packed byte holding the packet type and the argument count. See below.
</ResponseField>

<ResponseField name="Payload" type="argCount values">
  Encoded either by the event's schema or by the dynamic encoder, depending on packet type
  and whether a schema is registered.
</ResponseField>

## The header byte

The two-bit packet type sits in the high bits and the argument count in the low six.

| Bits       | Field          | Width  | Holds                                      |
| ---------- | -------------- | ------ | ------------------------------------------ |
| `7` to `6` | packet type    | 2 bits | `1` EVENT, `2` REQUEST, `3` RESPONSE       |
| `5` to `0` | argument count | 6 bits | `0` to `62` directly, or `63` as an escape |

<Note>
  Bit `7` is the most significant. The type occupies the top two bits because it is written
  as `packetType << 6`, so a header for an event with two arguments is `0x42`: `01` in the
  high bits, `000010` in the low six.
</Note>

```luau theme={null}
-- Write
if n < 63 then
	Buffers.WriteU8(w, bit32.bor(bit32.lshift(packetType, 6), n))
else
	Buffers.WriteU8(w, bit32.bor(bit32.lshift(packetType, 6), 63))
	Buffers.WriteVarInt(w, n)
end

-- Read
local h = Buffers.ReadU8(r)
local t = bit32.rshift(h, 6)
local c = bit32.band(h, 0x3F)
if c == 63 then c = Buffers.ReadVarInt(r) end
```

<Info>
  `63` is an escape value, not a count. Counts of 0 to 62 fit in the header byte directly;
  a count of 63 or more writes `63` in the field and follows it with a varint carrying the
  real count. So the header is one byte for essentially all real traffic.
</Info>

## Packet types

| Value | Type       | Direction        | Schema applied                                                                                                                                               |
| ----- | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `1`   | `EVENT`    | both             | <Tooltip tip="The event's schema encodes the payload when one is registered for this channel."><Icon icon="check" color="#16a34a" /></Tooltip> if registered |
| `2`   | `REQUEST`  | client to server | <Tooltip tip="Always uses the dynamic encoder, even when the channel has a schema."><Icon icon="xmark" color="#dc2626" /></Tooltip> never                    |
| `3`   | `RESPONSE` | server to client | <Tooltip tip="Always uses the dynamic encoder, even when the channel has a schema."><Icon icon="xmark" color="#dc2626" /></Tooltip> never                    |

<Note>
  Only two bits are allocated, so the format has room for one more packet type (`0` is
  unused). Values outside `1` to `3` are ignored by both dispatchers rather than raising an
  error.
</Note>

### EVENT

The payload is the arguments you passed to `Fire`, in order.

```
name("Damage"), header(type=1, n=2), Instance, u16
```

If a schema is registered for the name, each argument is encoded with `schema[i]`.
Otherwise each is tagged and encoded by `WriteAny`.

### REQUEST

Sent by `Client:Invoke`. The first payload value is the request id, followed by the invoke
arguments.

```
name("Shop"), header(type=2, n=1+argc), reqId, arg1, ... argN
```

<Info>
  The request id is a plain incrementing counter kept per channel name on the client. It is
  written with `WriteAny`, so a fresh session's early requests cost one tag plus one byte.
</Info>

### RESPONSE

Sent by the server after `OnInvoke` returns. The first two payload values are the request
id and a success flag, followed by the handler's return values.

```
name("Shop"), header(type=3, n=2+retc), reqId, ok, ret1, ... retN
```

<ResponseField name="reqId" type="number">
  Echoed back from the request so the client can find the parked thread.
</ResponseField>

<ResponseField name="ok" type="boolean">
  The first result of the server's `pcall`. When `false`, the next value is the error
  message and `Invoke` re-raises it.
</ResponseField>

<Note>
  The client accepts `true` or `1` as success when reading the flag, so the encoding
  tolerates the boolean arriving as a narrowed integer.
</Note>

## A worked example

Two events fired to the same player in one frame:

```luau theme={null}
Damage:Fire(player, humanoid, 250)   -- schema: { T.Instance(), T.UInt16() }
Chat:Fire(player, "gg")              -- no schema
```

They share one writer, so a single `FireClient` carries both packets back to back:

| Packet | Segment                     | Bytes  | Notes                                            |
| ------ | --------------------------- | ------ | ------------------------------------------------ |
| 1      | `varint(6)` plus `"Damage"` | 7      | channel name                                     |
| 1      | `0x42`                      | 1      | header, type `1`, argCount `2`                   |
| 1      | `varint(1)`                 | 1      | Instance, resolves to `insts[1]`                 |
| 1      | `u16(250)`                  | 2      | schema, so no type tag                           |
| 2      | `varint(4)` plus `"Chat"`   | 5      | channel name                                     |
| 2      | `0x41`                      | 1      | header, type `1`, argCount `1`                   |
| 2      | `TYPE_STRING`               | 1      | dynamic tag                                      |
| 2      | `varint(2)` plus `"gg"`     | 3      | the string                                       |
|        | **total**                   | **21** | plus `insts = { humanoid }` alongside the buffer |

<Tip>
  Note how much of that is the channel names: 12 of 21 bytes. At high frequency, shortening
  `"PlayerPositionUpdate"` to `"P"` is a real saving, and one no schema can give you.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Type tags" icon="hashtag" href="/concepts/wire-format/type-tags">
    Every dynamic-encoder tag and its cost.
  </Card>

  <Card title="Buffers API" icon="binary" href="/api/internals/buffers">
    The read and write primitives behind the format.
  </Card>
</CardGroup>
