kimo
EngineeringSeries: Kimo Bridge

Inside the incremental sync engine

Kimo moves about 1.2 billion rows a day from 57 kinds of sources into customer models. Here is how the incremental sync engine decides what to fetch, how it handles deletes and late data, and why we stopped trusting `updated_at`.

Théo Marchand
Co-founder & CTO14 min read

Every data product eventually rediscovers the same truth: a full refresh is the only sync strategy that is obviously correct, and it is too slow and too expensive for anything larger than a toy. Everything else is a trade-off between freshness, cost and correctness. This post is a tour of the trade-offs we made.

Three strategies, picked per table

The engine supports three strategies and chooses one per table, not per source. A single Postgres database might have a small plans table on full refresh, an events table on cursor sync, and an orders table on change data capture.

StrategyHow it worksCatches deletesTypical lag
Full refreshRe-read the whole table, swap atomicallyYesCadence
CursorRead rows where cursor > last seenNo (soft only)Cadence
CDCTail the database write-ahead logYesSeconds
Freshness lag at p95 by strategy
  • Full refresh (hourly)
  • Cursor (5 min)
  • CDC
Figure. Simulated fleet sample, minutes between a row changing at the source and becoming queryable in Kimo, by hour of day (UTC).

Cursors, and why `updated_at` lies

Cursor sync is simple: remember the highest updated_at you have seen, ask for everything above it next time. In practice updated_at breaks in at least four ways we see every week:

  • Not updated on every write — bulk UPDATE statements and ORM shortcuts skip the trigger.
  • Clock skew — rows written by replicas or app servers with drifting clocks.
  • Long transactions — a row with updated_at = 10:00:01 commits at 10:00:09, after we already read past 10:00:05.
  • Precision — timestamps truncated to the second, with thousands of rows sharing one value.

Our cursor reader handles the last two with a lookback window: each run re-reads a configurable overlap (default 10 minutes) behind the cursor and deduplicates on primary key. The cost is a little extra read volume; the benefit is that long transactions stop silently dropping rows.

Cursor read with lookback and tiebreaker
sql
select *
from public.orders
where (updated_at, id) > (:cursor_ts - interval '10 minutes', :cursor_id)
order by updated_at, id
limit 50000;

Change data capture

For Postgres, MySQL and SQL Server we read the database’s own change log: logical replication slots, the binlog, or CDC tables respectively. CDC gives us inserts, updates and deletes in commit order, with seconds of lag, and puts almost no load on the source.

Sources with native CDC support in the engine today.

CDC has its own sharp edges. A replication slot that is not consumed will hold WAL on the source until the disk fills — the single most dangerous failure mode in this whole system. We monitor slot lag continuously and, past a threshold the customer sets, drop the slot and fall back to a cursor sync with a full reconciliation, rather than risk their production database.

Landing the changes

Changes land in an append-only staging table, then merge into the modelled table in micro-batches. Each batch is idempotent: replaying it produces the same result, which makes retries and backfills safe.

Idempotent micro-batch merge
sql
merge into model.orders as t
using (
    select distinct on (id) *
    from staging.orders_changes
    where batch_id = :batch_id
    order by id, source_lsn desc
) as s
on t.id = s.id
when matched and s.op = 'delete' then
    delete
when matched and s.source_lsn > t.source_lsn then
    update set amount = s.amount, status = s.status,
               updated_at = s.updated_at, source_lsn = s.source_lsn
when not matched and s.op <> 'delete' then
    insert (id, amount, status, updated_at, source_lsn)
    values (s.id, s.amount, s.status, s.updated_at, s.source_lsn);

The source_lsn > t.source_lsn guard is what makes out-of-order delivery harmless: an older change can never overwrite a newer one.

By the numbers

1.2B
rows merged per day
41%
of tables on CDC
99.97%
batches merged on first try

Next on the roadmap: CDC for Snowflake and BigQuery via their change streams, and per-table freshness SLAs that surface in the UI as the same badges you see on dashboard tiles. If this is the kind of problem you like, we are hiring.

  • #Architecture
  • #CDC
  • #Postgres
Found this useful? Pass it on.
Written by
Théo Marchand
Co-founder & CTO at Kimo · 2 articles

Writes about Kimo Bridge, Security, Architecture, CDC.

People, companies and figures in this article are illustrative; charts use simulated data.

Put it to work

Go deeper

Whitepaper

Your Data, Your Rules

The hybrid analytics architecture behind Kimo Bridge: live query pushdown, optional cloud sync, and zero-trust by default.

22 pages
Live demo

Set up Kimo Bridge

Install the bridge, pick Bridge or Cloud mode per source, watch the audit log.

Simulated data · no sign-up

All resources
ArticleProduct
All

Introducing Kimo Bridge: your data stays home

A small package you install next to your database opens a private, outbound-only bridge to Kimo. Query live, store nothing — or sync to our cloud when you want to.

Théo Marchand
7 min read
Whitepaper
All

Your Data, Your Rules

The hybrid analytics architecture behind Kimo Bridge: live query pushdown, optional cloud sync, and zero-trust by default.

Arno Visser
22 pages

Your data officer is ready.

Connect a source — or install Kimo Bridge and keep data on your servers — then ask a question and get an answer you can audit.