Skip to content

Batch & Columnar Encoding

Batch encoding is where Twilic's advantage over MessagePack is largest. This guide explains row batches, columnar batches, batch sizing, and when each mode applies.

Row batch vs columnar batch

ModeWire tagBest for
Row batchrow_batchNested objects, moderate field count, general API lists
Columnar batchcol_batchTabular data, many rows, numeric/time-series columns

Both share shape and string interning. Columnar mode additionally compresses each field as an independent column.

Row batch

ts
import { encodeBatch } from "@twilic/core/advanced";

const users = page.map((u) => ({
id: BigInt(u.id),
name: u.name,
email: u.email,
role: u.role,
active: u.active,
}));

const bytes = encodeBatch(users);

What happens on the wire:

  1. Shape definition: field names registered once
  2. Each row encoded with shape reference (no repeated keys)
  3. Repeated strings ("admin", "us-east") interned across rows

Columnar batch

Ideal for telemetry, logs, and analytics events.

ts
const events = [
{ timestamp: 1700000000100n, service: "api", level: "info", duration_ms: 12 },
{ timestamp: 1700000000200n, service: "api", level: "info", duration_ms: 9 },
// ... up to 256+ events
];

const bytes = encodeBatch(events);
// Encoder selects col_batch when column regularity is high

Per-column codec selection:

Column typeTypical codecExample
Monotonic timestampDELTA_BITPACKLog event times
Low-cardinality stringDictionaryservice, level
Repeated value runsRLELog levels (info, info, info, …)
Numeric clusterFOR_BITPACK, SIMPLE8Bduration_ms, sensor readings
Homogeneous float arrayXOR_FLOATTemperature samples

Size comparison (256 events, 6 fields)

FormatSize
JSON~98 KB
MessagePack~65 KB
Twilic row batch~25–35 KB
Twilic col_batch~8–12 KB

Batch size tuning

PriorityBatch sizeTrade-off
Low latency16–64 recordsSmaller compression win
Balanced128–256 recordsGood default for APIs
Max compression512–1024 recordsHigher flush latency
Time series256–1024+Columnar codecs need volume

Rule of thumb: batch until compression gain plateaus on your payload — then measure P99 flush latency.

HTTP list endpoints

ts
import { encodeBatch } from "@twilic/core/advanced";
import { twilicResponse } from "@twilic/hono";

app.get("/users", async (c) => {
const users = await db.getUsers({ page, limit: 50 });
const bytes = encodeBatch(users);
return new Response(bytes, {
headers: { "Content-Type": "application/vnd.twilic" },
});
});

Or use a custom codec with createTwilicHono — see Hono integration.

Pipeline batching

text
[Agent: batch 64] → [Forwarder: batch 256] → [Kafka] → [Consumer]

Re-batch at each tier for cumulative compression. Do not re-encode individual events mid-pipeline unless necessary.

Anti-patterns

PatternProblem
Batch size = 1No interning benefit
Mixed shapes in one batchShape table churn, poor compression
Batch on HTTP error responsesOverhead exceeds benefit for tiny payloads
Columnar on deeply nested objectsRow batch is better

Language examples

Rust

rust
let bytes = twilic::encode_batch(&records)?;

Python

python
payload = twilic.encode_batch(events)

Go

go
bytes, err := twilic.EncodeBatch(records)

Released under the CC-BY-4.0 License.