π₯ Syncing¶
Want to read more?
I wrote my master's thesis on comparing syncing strategies.
Meerschaum efficiently syncs time-series data in three basic ETL steps: fetch, filter, and upsert.
Syncing Stages¶
The primary reason for syncing in this way is to take advantage of the properties of time-series data. Note that when writing your plugins, you only need to focus on getting data within begin and end bounds, and Meerschaum will handle the rest.
- Fetch (Extract and Transform) Because most new data will be newer than the previous sync, the fetch stage returns rows greater than the previous sync time (minus a backtracking window).
Example fetch query
- Filter (remove duplicates) After fetching the latest rows, the difference is taken to remove duplicates.
Skip filtering
To skip the filter stage, use --skip-check-existing or pipe.sync(check_existing=False).
Example filter query
-
Upsert (Load)
Once new rows are fetched and filtered, they are inserted into the database table via the pipe's instance connector. If
upsertis set in the pipe's parameters, then inserts and updates are performed in one query.
Example upsert queries
Backtracking¶
Depending on your data source, sometimes data may be missed. When rows are backlogged or a pipe contains multiple data streams (i.e. an ID column), a simple sync might overlook old data.
Add a backtrack interval¶
The backtrack interval is the overlapping window between syncs (default 1440 minutes).

In the example above, there are four data streams that grow at separate rates, the dotted lines represent remote data which have not yet been synced. By default, only data to the right of the red line will be fetched, which will miss data for the "slower" IDs.
You can modify the backtrack interval under the key fetch:backtrack_minutes:
Replacing a window¶
A sync normally merges: new rows are inserted, changed rows updated, and rows missing from the
new data are left alone. When the new data is the whole truth for a time range (a corrected
day of readings, a rollup recomputed from its source), replace that range instead: the rows of
[begin, end) are deleted and the new rows inserted in one transaction, so readers never
see the window half-written and a failed write leaves the old rows in place.
How the window is chosen:
- With data (
pipe.sync(df, replace=True)):beginandendare the window. A bound you do not pass comes from the data:beginis the earliest datetime indfandendjust after the latest, so every row you pass is inside. Rows outside[begin, end)are not written (the message says how many were skipped).paramslimits the window to the matching rows. - Without data (a fetch): the window is the fetch's own
[begin, end), by default the sync time minus the backtrack interval onward. Setfetch: {replace: true}to replace every fetched window, chunked syncs and Pipelines chunks included (each chunk replaces its own window). Rows the source returns outside the window are not written. The first sync, into a table that does not exist yet, has nothing to replace: its chunks are synced one by one like any fetch. - An empty frame with
beginandendclears the window. With neither, the sync is refused, so an empty frame never clears a table by accident. - A pipe without a
datetimecolumn has one window: the whole table (matchingparams).
The connectors' own sync_pipe(pipe, df, replace=True, begin=..., end=..., params=...) (SQL,
Valkey, api:) and the API's POST /pipes/{connector}/{metric}/{location}/data?replace=true
follow the same rules: a bound that is not given comes from the rows (begin=None over HTTP is
no bound on that side), and the API answers 400 for an empty body without begin and end.
In a Pipeline, a replacing sync marks its whole window as changed, so its children re-sync the window and drop the rows it deleted. A child's window ends at its parents' latest data, so rows newer than a parent's new latest row stay in the child until the child is rebuilt or cleared.
Instances
SQL instances delete and insert in one transaction. Valkey has no transactions: it clears the
window, then writes the rows. api: instances pass the window to the server, which replaces it
in one transaction on its own instance.
Verification Syncs¶
Occasionally it may be necessary to perform a more expensive verification sync across the pipe's entire interval. To do so, run verify pipes or sync pipes --verify:
A verification sync divides a pipe's interval into chunks and resyncs those chunks. Like the backtrack interval, you can configure the chunk interval under the keys verify:chunk_minutes:
When run without explicit date bounds, verification syncs are bounded to a maximum interval (default 366 days). This value may be set under verify:bound_days (or minutes, days, hours, etc.):
Deduplication Syncs¶
Although duplicates are removed during the filter stage of a sync, duplicate rows may still slip into your table if your data source returns duplicates.
Just like verification syncs, you can run deduplicate pipes to detect and delete duplicate rows. This works by deleting and resyncing chunks which contain duplicates.
Your instance connector must provide either clear_pipe() or deduplicate_pipe() methods to use pipe.deduplicate().
Combine --verify and --deduplicate.
You run mrsm sync pipes --verify --deduplicate to run verification and deduplication syncs in the same process.