Repository navigation
Description
Under which category would you file this issue?
Airflow Core
Apache Airflow version
3.3.2
What happened and how to reproduce it?
Issue Description
We've run into an issue relating to runs triggered by a large number of asset events. PATCH requests to the /execution/task-instances/{task_instance_id}/run (ti_run) takes time proportional to the number of asset events the DagRun consumes, and it does that work while holding a FOR UPDATE lock on the task_instance row.
When an asset-triggered run consumes thousands of events, every task start in that run is slow enough to hit statement timeouts, while the Task SDK's retries queue up behind the lock held by the previous attempt.
Code path on main (task_instances.py @ bca1e14):
- L182-L212:
SELECT ... FROM task_instance JOIN dag_run JOIN dag WHERE task_instance.id = :id FOR UPDATE OF task_instance. - The TI is updated to
RUNNING. - L322-L334: the DagRun is loaded with
.options(joinedload(DR.consumed_asset_events), ...). Only the collection itself is eager-loaded. - L366-L367:
TIRunContext(dag_run=dr, ...)is built. All of this runs in one transaction, so the row lock is held until commit.
This endpoint runs DagRun datamodel validations with asset lazy-loading (datamodels/taskinstance.py L362-L396) which causes numerous reads for asset and source_aliases on every AssetEventDagRunReference. Because of this the validation issues about one source_aliases SELECT per consumed event, all while the TI row lock is still held.
The scheduler does not run into the same issue because it avoids the pitfall (see _eager_load_dag_run_for_validation() )
Semi-related past issues and PRs: #65422, #59693, #66190.
How To Reproduce
- Create a DAG scheduled on a single asset, then pause it.
- Emit several thousand events for that asset, e.g. with repeated
POST /api/v2/assets/events. - Unpause the DAG. The scheduler creates one asset-triggered run that consumes all of the queued events.
- Watch the
ti_runlatency for the first task of that run. With SQLAlchemy echo or Postgres statement logging on, you see oneasset_alias_asset_event/source_aliasesSELECT per consumed event inside the transaction that holdsFOR UPDATE OF task_instance. Latency grows with the number of consumed events.
What you think should happen instead?
The ti_run execution API's endpoint latency, and particularly how long it holds the TI row lock, should not grow with the number of consumed asset events.
Possible fixes:
- Eager-load the nested relations in
ti_runin a similar way that the scheduler does via_eager_load_dag_run_for_validation()(see link in issue description above). That turns the N+1 queries into a fixed number of queries. - Load and serialize the consumed events after the TI state update is committed, so that work does not run under
FOR UPDATE. Alternatively, stop sending every consumed event inTIRunContextand have the Task SDK fetch them lazily whentriggering_asset_eventsis accessed.
Fix (1) is small and self-contained. Fix (2) also covers the cost of serializing large extra payloads.
Operating System
Linux (Debian-based container image)
Anything else?
This happens every time an asset-triggered run consumes a large number of events, which is common after a long pause of an asset-scheduled DAG whose upstream keeps producing events. Runs that consume only a few events are not noticeably affected.
Are you willing to submit PR?
- Yes I am willing to submit a PR!
Code of Conduct
- I agree to follow this project's Code of Conduct
Activity
arifrhm commentedon Nov 5, 2026
Hi! I'm working on this issue to eager-load the nested asset event relations in
ti_runsimilar to_eager_load_dag_run_for_validation(), resolving the N+1 query latency under the task instance row lock. I'll submit a PR with tests shortly.arifrhm commentedon Nov 5, 2026
I've opened a pull request to resolve this: #76565
It eager-loads both
AssetEvent.assetandAssetEvent.source_aliasesviaselectinloadinti_run, eliminating the N+1 queries during Pydantic response validation under theFOR UPDATElock, and adds test coverage for multiple consumed asset events.zach-overflow commentedon Nov 5, 2026
@arifrhm I opened this issue to gather input from the community on whether either of the 2 proposed solutions in my issue are the right approach, or whether there's some other approach that is preferable. Generally, these kinds of execution API + query adjustments require some prior discussion and evaluation, rather than immediately throwing an agent at the issue.
arifrhm commentedon Nov 5, 2026
Hi @zach-overflow, thanks for clarifying the context!
I completely agree that these execution API and transaction boundary decisions deserve careful discussion.
When looking at the two directions outlined in your issue:
_eager_load_dag_run_for_validation()in the scheduler, addressing the immediate N+1 statement timeouts inside theFOR UPDATElock without introducing breaking changes to the Task SDK wire protocol.extrafields), but involves deeper contract/protocol changes across Task SDK boundaries.I'm very happy to keep the PR as a concrete reference/PoC for Option 1 while we wait for feedback from maintainers (@ashb, @amoghrajesh) on which direction the community prefers. If the consensus leans towards Option 2 or a different architectural route, happy to pivot or follow the community's guidance!