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

# Buffers

> The internal encoding layer: pooled writers, readers, and every read and write pair.

<Info>
  This module is internal. It is documented so you can reason about how Flux behaves
  and performs, but it is not re-exported from the root `Flux` module and its surface
  may change without notice. For everyday use, reach for [`Flux.Server`](/api/server)
  and [`Flux.Client`](/api/client).
</Info>

```luau theme={null}
local Buffers = require(ReplicatedStorage.Flux.Utils.Buffers)
```

All wire I/O lives here. Compiled with `--!strict --!native --!optimize 2`, and every `buffer`
and `bit32` function is localised at the top of the module.

<Tip>
  This is the one internal module you have a legitimate reason to touch, because writing a
  [custom schema type](/api/types#custom-types) means calling these primitives directly.
</Tip>

## Types

<CodeGroup>
  ```luau Writer theme={null}
  export type Writer = {
  	buff: buffer,
  	cursor: number,
  	len: number,
  	insts: { Instance },
  	instIndex: { [Instance]: number },
  }
  ```

  ```luau Reader theme={null}
  export type Reader = {
  	buff: buffer,
  	cursor: number,
  	len: number,
  	insts: { Instance },
  }
  ```
</CodeGroup>

<ResponseField name="buff" type="buffer">
  The backing store. Reassigned on the writer when it grows.
</ResponseField>

<ResponseField name="cursor" type="number">
  Current byte offset. Advanced by every read and write.
</ResponseField>

<ResponseField name="len" type="number">
  On a writer, the allocated capacity. On a reader, the buffer's actual length, used as the loop
  bound when decoding a batch.
</ResponseField>

<ResponseField name="insts" type="{ Instance }">
  The side array carrying instance references out of band.
</ResponseField>

<ResponseField name="instIndex" type="{ [Instance]: number }">
  Writer only. Reverse lookup for deduplicating repeat references.
</ResponseField>

***

## Lifecycle

### CreateWriter / CreateReader

```luau theme={null}
function Buffers.CreateWriter(capacity: number?): Writer
function Buffers.CreateReader(buff: buffer, instances: { Instance }?): Reader
```

Both take from a free list before allocating. `capacity` defaults to `ALLOC_SIZE` (4096) and is
ignored when a pooled writer is available.

### FreeWriter / FreeReader

```luau theme={null}
function Buffers.FreeWriter(w: Writer): ()
function Buffers.FreeReader(r: Reader): ()
```

Resets the object and returns it to the pool. `FreeWriter` zeroes the cursor and clears both
instance tables. A writer whose capacity exceeded `MAX_REUSE_SIZE` (64 KB) is discarded rather
than pooled.

<Warning>
  Using a writer or reader after freeing it corrupts whichever send is holding it next. If you
  write a custom type, never call these, because the writer belongs to the `Bridge`.
</Warning>

### Finalize

```luau theme={null}
function Buffers.Finalize(w: Writer): (buffer, { Instance })
```

Copies the used `cursor` bytes into an exactly-sized buffer and clones the instance array.
Returns both, ready to pass to a remote.

<Info>
  The copy is necessary because the writer's backing buffer is about to be reused. This is the
  only allocation steady-state Flux performs, and it is unavoidable, since the remote needs a
  buffer it owns.
</Info>

### Growth

```luau theme={null}
local function Resize(w: Writer, needed: number)
	local newLen = w.len
	while w.cursor + needed > newLen do
		newLen = newLen * 2
	end
	-- allocate, copy w.cursor bytes, reassign
end
```

Capacity doubles until the pending write fits. Every public `Write*` checks
`cursor + size > len` first, so overflow is impossible.

***

## Primitives

Each pair reads and writes a fixed width, advancing the cursor by that many bytes.

| Write       | Read       | Bytes | Range                  |
| ----------- | ---------- | ----- | ---------------------- |
| `WriteU8`   | `ReadU8`   | 1     | `0` to `255`           |
| `WriteU16`  | `ReadU16`  | 2     | `0` to `65,535`        |
| `WriteU32`  | `ReadU32`  | 4     | `0` to `4,294,967,295` |
| `WriteI8`   | `ReadI8`   | 1     | `-128` to `127`        |
| `WriteI16`  | `ReadI16`  | 2     | `-32,768` to `32,767`  |
| `WriteI32`  | `ReadI32`  | 4     | `-2^31` to `2^31-1`    |
| `WriteF32`  | `ReadF32`  | 4     | about 7 digits         |
| `WriteF64`  | `ReadF64`  | 8     | lossless               |
| `WriteBool` | `ReadBool` | 1     | `u8`, reads as `== 1`  |

<Warning>
  These delegate straight to `buffer.write*`, which does not validate. Out-of-range values
  silently truncate to the low bits, and non-integers truncate toward zero.
</Warning>

### Varints

```luau theme={null}
function Buffers.WriteVarInt(w: Writer, v: number): ()
function Buffers.ReadVarInt(r: Reader): number
```

LEB128-style: seven payload bits per byte, high bit set to continue. Unsigned only.

| Value                   | Bytes |
| ----------------------- | ----- |
| `0` to `127`            | 1     |
| `128` to `16,383`       | 2     |
| `16,384` to `2,097,151` | 3     |

### Strings and buffers

```luau theme={null}
function Buffers.WriteString(w: Writer, v: string): ()
function Buffers.ReadString(r: Reader): string

function Buffers.WriteBuffer(w: Writer, v: buffer): ()
function Buffers.ReadBuffer(r: Reader): buffer
```

Both write a varint length followed by raw bytes. `WriteString` uses `buffer.writestring`, so
binary content is safe and lengths are in bytes rather than characters.

***

## Roblox datatypes

| Pair                                 | Encoding                          | Bytes      | Lossless                                                                                                                                                                           |
| ------------------------------------ | --------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WriteVector2` / `ReadVector2`       | 2 `f32`                           | 8          | <Tooltip tip="Caveats apply: components are sent as 32-bit floats, so full Luau number precision is not preserved."><Icon icon="triangle-exclamation" color="#d97706" /></Tooltip> |
| `WriteVector3` / `ReadVector3`       | 3 `f32`                           | 12         | <Tooltip tip="Caveats apply: components are sent as 32-bit floats, so full Luau number precision is not preserved."><Icon icon="triangle-exclamation" color="#d97706" /></Tooltip> |
| `WriteCFrame` / `ReadCFrame`         | position plus axis-angle, 7 `f32` | 28         | <Tooltip tip="Lossy: rotation is sent as axis-angle and rebuilt, so it will not match the original matrix bit-for-bit."><Icon icon="xmark" color="#dc2626" /></Tooltip>            |
| `WriteColor3` / `ReadColor3`         | 3 `u8`                            | 3          | <Tooltip tip="Lossy: each channel is quantised to 8 bits with a downward floor bias."><Icon icon="xmark" color="#dc2626" /></Tooltip>                                              |
| `WriteUDim` / `ReadUDim`             | `f32` plus `i32`                  | 8          | <Tooltip tip="Caveats apply: the scale is sent as a 32-bit float, so full Luau number precision is not preserved."><Icon icon="triangle-exclamation" color="#d97706" /></Tooltip>  |
| `WriteUDim2` / `ReadUDim2`           | 2 UDim                            | 16         | <Tooltip tip="Caveats apply: the scale is sent as a 32-bit float, so full Luau number precision is not preserved."><Icon icon="triangle-exclamation" color="#d97706" /></Tooltip>  |
| `WriteRegion3` / `ReadRegion3`       | CFrame plus Vector3               | 40         | <Tooltip tip="Lossy: rebuilt axis-aligned from position and size, discarding any rotation."><Icon icon="xmark" color="#dc2626" /></Tooltip>                                        |
| `WriteInstance` / `ReadInstance`     | varint index                      | 1 to 3     | <Tooltip tip="Caveats apply: arrives as nil if the receiver cannot see the instance."><Icon icon="triangle-exclamation" color="#d97706" /></Tooltip>                               |
| `WriteEnum` / `ReadEnum`             | `"Enum.X.Y"` string               | 1 plus len | <Tooltip tip="Round-trips exactly: the value received equals the value sent."><Icon icon="check" color="#16a34a" /></Tooltip>                                                      |
| `WriteBrickColor` / `ReadBrickColor` | `u16` of `.Number`                | 2          | <Tooltip tip="Round-trips exactly: the value received equals the value sent."><Icon icon="check" color="#16a34a" /></Tooltip>                                                      |
| `WriteTweenInfo` / `ReadTweenInfo`   | 3 `f64`, 2 enums, `bool`          | about 50   | <Tooltip tip="Round-trips exactly: the value received equals the value sent."><Icon icon="check" color="#16a34a" /></Tooltip>                                                      |

<Note>
  `WriteUDim`, `WriteUDim2`, `WriteRegion3`, `WriteBrickColor`, `WriteTweenInfo` and
  `WriteBuffer` are reachable only through `WriteAny`, because the [`Types`](/api/types)
  namespace has no constructor for them. Call them directly from a custom type if you need them
  under a schema.
</Note>

<Warning>
  Three of these are lossy in ways worth knowing.

  `ReadCFrame` rebuilds with `CFrame.fromAxisAngle(axis, angle) + pos`, which is not bit-exact
  and is degenerate for zero rotation. `ReadColor3` quantises to 8 bits per channel with a
  downward `floor` bias. `ReadRegion3` rebuilds axis-aligned from position plus or minus half
  the size, discarding the original CFrame's rotation entirely.

  See [Serialization](/concepts/serialization#lossy-conversions).
</Warning>

### Instance handling

```luau theme={null}
function Buffers.WriteInstance(w: Writer, v: Instance): ()
function Buffers.ReadInstance(r: Reader): Instance?
```

`WriteInstance` appends to `w.insts`, or reuses the existing `w.instIndex[v]`, and writes a
varint index. Nothing about the instance enters the buffer.

<Warning>
  `ReadInstance` returns `r.insts[idx]`, which is `nil` whenever the receiver cannot see the
  instance. Note the return type is `Instance?` while `Types.Instance()` is typed
  `Type<Instance>`, so the non-optional annotation is optimistic. Always nil-check.
</Warning>

### Enum caching

`WriteEnum` memoises `tostring(item)` in `EnumCache`, and `ReadEnum` memoises the reverse in
`StringToEnumCache`, resolving cache misses by splitting on `.` and indexing `Enum`. An
unresolvable name returns `nil` rather than erroring.

***

## Packet header

```luau theme={null}
function Buffers.WritePacketHeader(w: Writer, packetType: number, n: number): ()
function Buffers.ReadPacketHeader(r: Reader): (number, number)
```

Packs a two-bit type into the high bits of one byte and the argument count into the low six. A
count of 63 or more writes `63` as an escape and follows with a varint.

See [Packets](/concepts/wire-format/packets#the-header-byte).

***

## Dynamic encoder

```luau theme={null}
function Buffers.WriteAny(w: Writer, v: any): ()
function Buffers.ReadAny(r: Reader): any
```

Writes a one-byte type tag then the value, dispatching on `typeof(v)`. Used for every
unschematised event argument and for all invoke traffic.

<AccordionGroup>
  <Accordion title="Integer narrowing" icon="compress">
    Integers are written at the narrowest width that fits: `u8`, `u16` or `u32` when
    non-negative, `i16` or `i32` when negative, and `f64` otherwise. `WriteI8` is never
    selected, so small negatives cost two bytes.
  </Accordion>

  <Accordion title="Table classification" icon="table">
    A single `pairs` walk decides array against dictionary, breaking on the first key that is not
    an integer in `1..n`. Arrays get `TYPE_ARRAY` plus a varint count; dictionaries get
    `TYPE_TABLE` and alternating key and value pairs terminated by a `nil` key.
  </Accordion>

  <Accordion title="Circular references overflow" icon="triangle-exclamation">
    Recursion has no visited-set and no depth limit, so a table referencing itself recurses until
    the Luau stack overflows.
  </Accordion>

  <Accordion title="Unsupported types degrade silently" icon="circle-exclamation">
    An unrecognised `typeof` logs `warn("Unsupported type: " .. t)` on the sender and writes
    `TYPE_NIL`. The receiver gets `nil` with no indication anything went wrong, and the argument
    count still matches.
  </Accordion>
</AccordionGroup>

See the [full tag table](/concepts/wire-format/type-tags).

***

## Pack / Unpack

```luau theme={null}
function Buffers.Pack(w: Writer, ...: any): ()
function Buffers.Unpack(r: Reader): { any }
```

Writes a varint count followed by that many `WriteAny` values. `Unpack` returns them as an
array.

<Note>
  Public but unused by Flux itself, because the `Server` and `Client` modules write their argument
  counts through the packet header instead. Available if you are building your own framing on top
  of `Buffers`.
</Note>

***

## Constants

| Constant         | Value   | Meaning                                      |
| ---------------- | ------- | -------------------------------------------- |
| `ALLOC_SIZE`     | `4096`  | Default writer capacity                      |
| `POOL_SIZE`      | `512`   | Maximum pooled writers and readers           |
| `MAX_REUSE_SIZE` | `65536` | Writers above this are discarded, not pooled |

<Info>
  4 KB comfortably holds a typical frame of traffic, which is why steady-state Flux performs no
  buffer allocation. Writers cycle between the pool and the bridge, and only `Finalize`
  allocates.
</Info>

## Next steps

<CardGroup cols={2}>
  <Card title="Custom types" icon="wrench" href="/api/types#custom-types">
    Using these primitives in a schema entry.
  </Card>

  <Card title="Type tags" icon="hashtag" href="/concepts/wire-format/type-tags">
    Every tag and its byte cost.
  </Card>
</CardGroup>
