Skip to content

Runs with many consumed asset events lead to Task execution API timeouts #76558

Open

Description

@zach-overflow

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):

  1. L182-L212: SELECT ... FROM task_instance JOIN dag_run JOIN dag WHERE task_instance.id = :id FOR UPDATE OF task_instance.
  2. The TI is updated to RUNNING.
  3. L322-L334: the DagRun is loaded with .options(joinedload(DR.consumed_asset_events), ...). Only the collection itself is eager-loaded.
  4. 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

  1. Create a DAG scheduled on a single asset, then pause it.
  2. Emit several thousand events for that asset, e.g. with repeated POST /api/v2/assets/events.
  3. Unpause the DAG. The scheduler creates one asset-triggered run that consumes all of the queued events.
  4. Watch the ti_run latency for the first task of that run. With SQLAlchemy echo or Postgres statement logging on, you see one asset_alias_asset_event / source_aliases SELECT per consumed event inside the transaction that holds FOR 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:

  1. Eager-load the nested relations in ti_run in 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.
  2. 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 in TIRunContext and have the Task SDK fetch them lazily when triggering_asset_events is 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

Activity

  1. arifrhm commented on Nov 5, 2026

    @arifrhm

    Hi! I'm working on this issue to eager-load the nested asset event relations in ti_run similar 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.

  2. arifrhm commented on Nov 5, 2026

    @arifrhm

    I've opened a pull request to resolve this: #76565

    It eager-loads both AssetEvent.asset and AssetEvent.source_aliases via selectinload in ti_run, eliminating the N+1 queries during Pydantic response validation under the FOR UPDATE lock, and adds test coverage for multiple consumed asset events.

  3. zach-overflow commented on Nov 5, 2026

    @zach-overflow
    ContributorAuthor

    @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.

  4. arifrhm commented on Nov 5, 2026

    @arifrhm

    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:

    • Option 1 (the PR): Looked like a lightweight, low-risk fix/PoC that mirrors _eager_load_dag_run_for_validation() in the scheduler, addressing the immediate N+1 statement timeouts inside the FOR UPDATE lock without introducing breaking changes to the Task SDK wire protocol.
    • Option 2 (deferring event loading / Task SDK lazy fetch): Solves the broader issue (including the payload serialization cost of large extra fields), 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:bugThis is a clearly a bugneeds-triagelabel for new issues that we didn't triage yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions