# Message batches

> The batch layout messages travel and rest in, from the 256-byte batch header down to individual message frames.

Rendered page: https://iggy.apache.org/docs/binary-protocol/messages/

Source: https://github.com/apache/iggy-website/blob/main/content/docs/binary-protocol/messages.mdx

Messages travel and rest in exactly one layout, the **batch**: the `SendMessages` body, the replicated record, the persisted segment record, and the poll reply all share it. There is no other message encoding.

```text
[batch header: 256 bytes][blob: message frames]
frame = [frame header: 48 bytes][payload][user_headers]
```

## Batch header (256 bytes)

| Offset | Size | Field | Type | Description |
|--------|------|-------|------|-------------|
| 0 | 8 | `partition_id` | u64 | Server-stamped at admission. Producers send zero. |
| 8 | 8 | `base_offset` | u64 | Server-stamped when the batch is appended to the partition journal. Producers send zero. |
| 16 | 8 | `base_timestamp` | u64 | Server-stamped at journal append (microseconds). Producers send zero. |
| 24 | 8 | `origin_timestamp` | u64 | Producer-set: the earliest `origin_timestamp` among the batch's messages (microseconds). |
| 32 | 8 | `batch_length` | u64 | Total batch size: 256 + blob length. |
| 40 | 8 | `batch_checksum` | u64 | XxHash3-64 over the header fields (`partition_id`, `base_offset`, `base_timestamp`, `origin_timestamp`, `batch_length`, `message_count`, in that order, little-endian bytes) followed by each frame's 8 checksum bytes. Recomputed by the server when it stamps the header. |
| 48 | 4 | `message_count` | u32 | Number of frames in the blob. |
| 52 | 204 | reserved | bytes | Must be zero; the server rejects a batch with nonzero reserved bytes. |

## Message frame header (48 bytes)

| Offset | Size | Field | Type | Description |
|--------|------|-------|------|-------------|
| 0 | 8 | `checksum` | u64 | XxHash3-64 over frame bytes 8..48 followed by `payload` and `user_headers`. Producer-computed; verified at admission. |
| 8 | 16 | `id` | u128 | Message id (for example a UUID). |
| 24 | 4 | `offset_delta` | u32 | Message offset = `base_offset + offset_delta`. Producer sends the frame's index in the batch. |
| 28 | 4 | `timestamp_delta` | u32 | Microsecond delta against the batch `origin_timestamp`. One batch spans at most about 71.6 minutes of producer clock (`u32` microseconds). |
| 32 | 4 | `user_headers_length` | u32 | Byte length of the user headers block. |
| 36 | 4 | `payload_length` | u32 | Byte length of the payload. |
| 40 | 8 | reserved | u64 | Must be zero. |

After the 48 header bytes: `payload` (`payload_length` bytes), then `user_headers` (`user_headers_length` bytes). The payload comes **first**.

A frame's absolute position is derived, not stored: offset = `base_offset + offset_delta`. The server-stamped time is the flat batch `base_timestamp` (the delta does not apply to it); producer time resolves as `origin_timestamp + timestamp_delta`.

The 64-byte `IggyMessageHeader` (checksum, id, offset, timestamp, origin_timestamp, lengths, reserved) that earlier protocol generations put in front of every message **no longer exists on the wire or on disk**. It survives only as an in-memory model.
