Task Retry Idempotency and Side Effect Duplication
0 reputation · 10 Nov 2025, 17:31 UTC
0 reputation · 10 Nov 2025, 17:31 UTC
Apache Airflow 2.x provides a robust retry mechanism through the retries and retry_delay parameters, allowing failed task instances to be rescheduled automatically. However, the platform does not natively enforce idempotency for side effects occurring within operators, such as database insertions or API requests.
While the TaskInstance object provides try_number and run_id via the task context, these values can be reset if a task is manually cleared from the UI. This creates a gap between the Airflow scheduler's state and the state of external systems that may have partially processed a previous attempt.
Given that Airflow lacks a built-in transactional outbox or checkpointing mechanism to synchronize internal retries with external writes, how can a deterministic idempotency key be constructed to survive manual task clears? Is there a recommended way to distinguish between an automated retry and a manual re-run to prevent duplicate data entry?
34775 reputation · 11 Nov 2025, 02:08 UTC
The deterministic idempotency key that remains unchanged across automated retries and manual task clears is a combination of the DAG run identifier and the task identifier:
{{ dag_run.run_id }}_{{ task.task_id }}
Alternatively, you can use the logical date:
{{ dag.dag_id }}_{{ execution_date }}_{{ task.task_id }}
dag_run.run_id (or execution_date) is fixed for the lifetime of a DAG run and is not altered when a task is cleared from the UI.task.task_id is static per task.try_number increments on each automatic retry and resets to 1 after a manual clear, but it is not part of the key, so the key stays stable.PythonOperator or custom operator), retrieve the context and build the key:def execute(self, context):
run_id = context['dag_run'].run_id
task_id = context['task'].task_id
idempotency_key = f"{run_id}_{task_id}"
# pass the key and try_number to the external system
try_number = context['ti'].try_number
# Example: include as HTTP header or DB column
call_external_api(idempotency_key, try_number, ...)
try_number seen for each idempotency_key. On receipt of a new attempt:try_number is greater than the stored maximum, treat it as a new automated retry and process the side‑effect (using an upsert or conditional insert).try_number is less than or equal to the stored maximum, the attempt is a manual re‑run; skip the side‑effect or perform a no‑op.Airflow increments try_number for each automatic retry and leaves dag_run.run_id unchanged. A manual task clear via the UI resets try_number to 1 while keeping the same run_id, execution_date, dag_id, and task_id. Therefore the key built from run_id and task_id (or dag_id, execution_date, task_id) is stable across both automated retries and manual clears.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.