Skip to content

Batch & Columnar Encoding

Batch encoding is where Twilic's advantage over repeated structure is largest. This guide explains dynamic row batches, dynamic columnar batches, v3 SCHEMA_BATCH, 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
Schema batchSCHEMA_BATCH (0x0E)Shared-schema tabular records and Protobuf/Avro comparisons

Dynamic row/column batches share shape and string interning. Columnar mode additionally compresses each field as an independent column. SCHEMA_BATCH uses a shared schema, schema-order columns, presence bitmaps, and typed-vector payloads.

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

The v3 reference interoperability profile defines payload grammar for PLAIN and DIRECT_BITPACK. Other registered codec codes require a negotiated codec profile before interoperable results or benchmark claims use them.

Schema batch

Use SCHEMA_BATCH when every row is represented by one shared schema:

text
0x0E [schema_id?][count][column_count?][columns...]

In the v3 reference profile, column_count is present and field_id is omitted in strict schema-order compact mode. Optional/nullable columns use row-order presence bitmaps, and typed-vector payloads encode present values only.

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

For schema-fixed homogeneous records, compare SCHEMA_BATCH against Protobuf repeated messages or Avro raw streams using equivalent schema and framing assumptions.

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.