A single row write in MongrelDB costs about four and a half microseconds, and making that same row durable costs about four and a half milliseconds, which means the fsync at the end of a commit is roughly a thousand times more expensive than the work it protects, and every ingest pipeline that commits per row lives and dies inside that gap. This is not a MongrelDB quirk, it is the physics of asking a disk to promise something, and the only honest answers any storage engine has ever found are to batch, to lie about durability, or to be slow, so the interesting question is never whether to batch but who holds the buffer and under what contract. Kit’s answer is WriteBuffer, a small opt-in object that collects rows in process memory and commits them as one transaction, and this post walks through why it exists, what it actually does when you call it, and what you give up to get the throughput.
The fsync is the tax
The numbers from the MongrelDB benchmark suite make the shape of the problem obvious, because a put without fsync lands in 4.4828 microseconds while a commit with fsync takes 4.6721 milliseconds, so a client that commits after every row tops out near two hundred durable rows per second no matter how fast the engine itself is. Batch a thousand puts into a single commit and the same benchmark finishes in 7.7071 milliseconds, which is about 130 thousand rows per second, and the million-row fixture, which commits the entire batch as one transaction rather than looping thousand-row flushes, completes in 854.47 milliseconds, about 1.17 million rows per second on the measured hardware, so between those two points you can see the amortization curve a WriteBuffer threshold is really tuning. Those measurements come from release builds on Linux x86-64 with an Intel Core Ultra 9 386H and 62 GiB of RAM, collected with Criterion in July 2026, so treat them as one machine’s honest engineering numbers rather than a promise about yours, but the three-orders-of-magnitude spread between the slow path and the fast path will reproduce anywhere, because it is the fsync you are paying for and not the row.
What WriteBuffer actually is
The Kit surface is deliberately tiny, db.writeBuffer(table, threshold?) hands you a buffer over one table, put(cells) stages a row, and flush() commits everything staged so far, with an automatic flush firing when the buffer reaches threshold rows, one thousand by default:
const wb = db.writeBuffer("events", 1000);
for (const event of stream) {
const flushed = wb.put([
{ columnId: 1, int64: event.id },
{ columnId: 2, text: event.kind },
{ columnId: 3, json: event.payload },
]);
// `flushed` is null unless this put crossed the threshold,
// in which case it is the epoch of the auto-flush commit.
}
const epoch = wb.flush(); // durable after this line
The return values are worth reading twice, because put returns null almost every call and only hands back the commit epoch on the call that triggered an auto-flush, while flush() always returns the epoch it created, or the current epoch untouched if the buffer was empty, so an empty flush is a cheap no-op rather than a wasted fsync. Underneath the TypeScript there is no cleverness to speak of, just a Vec of cell pairs sitting in process memory, and when the flush comes the whole vector moves into one put_batch inside a single transaction, which means one WAL append and one fsync for the whole threshold’s worth of rows, the exact same fast-commit path we covered in the WAL article, amortized across a thousand rows instead of paid per row.
The contract is flipped on purpose
Here is the part that should make you pause before reaching for it, a normal put() in Kit is durable by the time it returns, and a WriteBuffer.put() is durable only after a flush has completed, so a crash with nine hundred buffered rows loses all nine hundred of them and the database will not apologize. That inversion is the entire product, because you cannot amortize a commit you insist on performing, and it is also why the buffer is a separate object you have to ask for by name instead of a flag on the table, since a durability downgrade that arrives by default is a bug report waiting to happen. The practical consequence is that WriteBuffer belongs in pipelines where the source is replayable, log shipping, ETL jobs, backfills, sync workers that checkpoint their offsets after each flush, and it does not belong anywhere a user is staring at a spinner waiting to know their write is safe.
What it skips, and why that is fine
The second thing you trade away is Kit’s constraint layer, because the buffered path bypasses the defaults, unique guards, and foreign-key checks that Kit applies around ordinary writes, while the engine’s own commit-time enforcement, the same checks the constraints article walks through, still sees the flushed batch, since a flush is an ordinary transaction underneath. That sounds alarming until you remember the intended caller, an ingest path that already validated its rows upstream, where paying a second round of constraint checks per row is the tax you built the buffer to avoid, and if that description does not match your workload then the bypass is telling you, correctly, that you wanted put() all along. Sizing the threshold is the last knob, and it is really a memory-and-loss decision rather than a performance one, since the default thousand rows bounds both the RAM the buffer can hold and the worst case of what a crash can take with it, so raise it when your rows are small and your source replays cheaply, and lower it when the opposite is true.
The old answer, again
None of this is new thinking, MySQL’s group commit and Kafka’s batch.size and every WAL-based engine since Postgres all arrived at the same place, because the fsync wall does not negotiate, and the only design freedom left is where the buffer lives and how honest the API is about what a crash will cost you. Kit’s version keeps the buffer in your process, keeps the commitment in one transaction, and keeps the contract printed on the method signature, durable after flush(), not before, which is the kind of boring honesty that lets an ingest pipeline run at a million rows a second without anyone pretending the physics went away.