Scheduling, Idempotency, and Incremental Runs
Learning objective: design reruns safely; understand full refresh vs incremental processing.
A scheduled run kicks off at 2am and fails halfway through because of a network blip. The on-call engineer manually retriggers it, and the next morning you discover the same batch_id was counted twice in the output table, doubling the total. This is a classic non-idempotent pipeline: rerunning the same input did not overwrite or replace the previous result, it stacked on top of it. Designing safe reruns is a baseline requirement for any scheduled pipeline, not an advanced option.
# Wrong: every run appends, so a rerun creates duplicate rows
result.to_sql("monthly_summary", con=engine, if_exists="append")
# Right: key the write by the run's processing range, delete the same range's old
# result first, then write the new one (idempotent)
def publish_idempotent(result: pd.DataFrame, period: str, engine) -> None:
with engine.begin() as conn:
conn.execute(
"DELETE FROM monthly_summary WHERE period = :period",
{"period": period}
)
result.to_sql("monthly_summary", con=conn, if_exists="append", index=False)
# An incremental run needs to track how far it has already processed (a watermark)
def get_watermark(engine) -> str:
row = engine.execute("SELECT MAX(period) AS last_period FROM monthly_summary").first()
return row.last_period if row and row.last_period else "1970-01"| Approach | Good fit | Risk |
|---|---|---|
| Full refresh | Small data volumes; source occasionally revises historical data | Runtime and cost grow as data volume grows |
| Incremental | Large, stable data volumes that only ever append | Must track a watermark correctly and handle late-arriving data |
Besides "delete the same period then write," another common pattern is to upsert on a key such as (source_system, period, metric): update the row if the key already exists, insert it if not. Both approaches keep the output table consistent no matter how many times the same input batch is rerun; which one to use depends on whether the database makes upsert syntax convenient and whether a recompute always covers a whole period at once.
- Check whether the write is idempotent
Rerunning the same input twice should leave the output table exactly the same, never doubled. - Choose full refresh or incremental
Decide based on data volume and whether the source revises history, whether to recompute everything or just the new part. - Track a watermark
An incremental pipeline needs to record how far it has already processed, as the starting point for the next run. - Handle late-arriving data
Design a mechanism so data that arrives later than expected is still correctly attributed to its period.
Warning
A scheduled run fails from a network issue, an on-call engineer manually reruns the same batch, and the output table’s total doubles. What design is this pipeline missing?