Apache Airflow version
3.0.6
What happened?
Deferred DbtCloudRunJobOperator tasks fail almost immediately with a misleading DbtCloudJobRunException: Job run <id> has failed. even though the real dbt Cloud job is still running (and later completes successfully, sometimes hours later). The task fails within ~10 seconds of being deferred — far too fast to reflect a real terminal job state for a job that in fact took several more hours to finish.
I traced this down to a real bug in Airflow core (Task SDK), not in the dbt Cloud provider itself, and not specific to any one secrets backend implementation. Full trace below.
Environment
- Deployment: Amazon MWAA
secrets.backend: airflow.providers.amazon.aws.secrets.secrets_manager.SecretsManagerBackend
secrets.backend_kwargs: {"config_prefix":"airflow/config","connections_prefix":"airflow/connections","variables_prefix":"airflow/variables"}
- Connection
dbt_cloud (conn_type dbt_cloud) has password populated (used by the provider as the dbt Cloud API token) and no extra.
- The task is
DbtCloudRunJobOperator(..., deferrable=True) using the connection above.
Observed log sequence
Worker (synchronous execute(), before deferring) — succeeds fine:
{"logger": "airflow.hooks.base", "event": "Connection Retrieved 'dbt_cloud'", "level": "info"}
{"logger": "...DbtCloudHook", "event": "Getting the status of job run <job_run_id>.", "level": "info"}
{"logger": "...DbtCloudHook", "event": "Current status of job run <job_run_id>: QUEUED", "level": "info"}
{"event": "Pausing task as DEFERRED.", "level": "info"}
Triggerer (async DbtCloudRunJobTrigger.run() poll cycle) — crashes:
{"event": "trigger <dag_id>/<run_id>/dbt_cloud_task/-1/1 (ID <n>) starting", "level": "info"}
{"logger": "...DbtCloudHook", "event": "Getting the status of job run <job_run_id>.", "level": "info"}
{
"logger": "task",
"error_detail": [{
"exc_type": "RuntimeError",
"exc_value": "You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.",
"frames": [
{"filename": ".../airflow/sdk/execution_time/context.py", "lineno": 131, "name": "_get_connection"},
{"filename": ".../airflow/secrets/base_secrets.py", "lineno": 79, "name": "get_connection"},
{"filename": ".../airflow/secrets/base_secrets.py", "lineno": 65, "name": "deserialize_connection"},
{"filename": ".../airflow/models/connection.py", "lineno": 557, "name": "from_json"},
{"filename": "<string>", "lineno": 4, "name": "__init__"},
{"filename": ".../sqlalchemy/orm/state.py", "lineno": 481, "name": "_initialize_instance"},
{"filename": ".../sqlalchemy/util/langhelpers.py", "lineno": 70, "name": "__exit__"},
{"filename": ".../sqlalchemy/util/compat.py", "lineno": 211, "name": "raise_"},
{"filename": ".../sqlalchemy/orm/state.py", "lineno": 479, "name": "_initialize_instance"},
{"filename": ".../airflow/models/connection.py", "lineno": 177, "name": "__init__"},
{"filename": ".../airflow/sdk/execution_time/secrets_masker.py", "lineno": 134, "name": "mask_secret"},
{"filename": ".../airflow/jobs/triggerer_job_runner.py", "lineno": 740, "name": "send"},
{"filename": ".../asgiref/sync.py", "lineno": 186, "name": "__call__"}
]
}],
"event": "Task failed with exception",
"level": "error"
}
{"event": "Unable to retrieve connection from secrets backend (SecretsManagerBackend). Checking subsequent secrets backend.", "level": "error"}
{"result": "TriggerEvent<{'status': 'error', 'message': 'You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.', 'run_id': <job_run_id>}>", "event": "Trigger fired event", "level": "info"}
{"event": "trigger completed", "level": "info"}
This TriggerEvent(status='error', ...) is then consumed by DbtCloudRunJobOperator.execute_complete(), which raises DbtCloudJobRunException: Job run <job_run_id> has failed. — a message that implies the dbt Cloud job failed, when in reality Airflow never actually observed its terminal state at all.
Confirmed independently via the dbt Cloud Admin API that the underlying run continued after Airflow marked the task failed, and later reached a real terminal state (Success) several hours afterward — proving the "has failed" exception was not based on any actual observed job outcome.
Root cause (traced against apache/airflow tag 3.0.6)
This is not a dbt Cloud provider bug, and not specific to SecretsManagerBackend. It's in _get_connection(), task-sdk/src/airflow/sdk/execution_time/context.py:
def _get_connection(conn_id: str) -> Connection:
backends = ensure_secrets_backend_loaded()
for secrets_backend in backends:
try:
conn = secrets_backend.get_connection(conn_id=conn_id)
if conn:
if conn.password:
mask_secret(conn.password) # <-- crashes here
if conn.extra:
mask_secret(conn.extra)
return conn
except Exception:
log.exception(
"Unable to retrieve connection from secrets backend (%s). "
"Checking subsequent secrets backend.",
type(secrets_backend).__name__,
)
...
msg = SUPERVISOR_COMMS.send(GetConnection(conn_id=conn_id)) # <-- same crash, uncaught here
secrets_backend.get_connection(conn_id) succeeds — the connection is fetched and deserialized fine.
mask_secret(conn.password) is called next. mask_secret() (task-sdk/src/airflow/sdk/execution_time/secrets_masker.py:132-134) does:
if comms := getattr(task_runner, "SUPERVISOR_COMMS", None):
comms.send(MaskSecret(value=secret, name=name))
- Inside the triggerer,
SUPERVISOR_COMMS is the triggerer's comms decoder (per airflow/jobs/triggerer_job_runner.py). Calling .send() on it from inside a coroutine that is already running the active event loop conflicts with the sync→async bridge (asgiref.sync.AsyncToSync) it uses internally, raising the RuntimeError.
- This exception is caught by the
except Exception in the loop (logged as "Checking subsequent secrets backend"), but since only one backend is configured, the loop ends. Execution falls through to the unguarded fallback call SUPERVISOR_COMMS.send(GetConnection(conn_id=conn_id)) at line 160 — the exact same .send() primitive, called from the exact same broken async context — which raises the identical RuntimeError, this time uncaught, propagating up into the trigger's run() coroutine and surfacing as a generic TriggerEvent(status='error').
Key point: this has nothing to do with which secrets backend is configured, or which Connection field holds the secret. Any deferred operator that (a) uses a Connection with password or extra populated, and (b) re-fetches that connection from inside the triggerer's async run() loop (as DbtCloudRunJobTrigger does on every poll, via provide_account_id's sync_to_async(self.get_connection)(...) call), will hit this. It happens to surface here via apache-airflow-providers-dbt-cloud, but the defect is in Airflow core / Task SDK, not the provider.
Related reports
This report differs from the above in that the trace above shows the exact same failure mode reproduced independently three times in the same ~20 second window (three separate deferred DbtCloudRunJobOperator tasks polling around the same time), each with a matching stack trace ending in secrets_masker.py:134 mask_secret → triggerer_job_runner.py:740 send → asgiref/sync.py:186 AsyncToSync.__call__, and independently confirmed against the downstream API that the reported "job run has failed" was not based on a real observed terminal state for at least two of the three runs.
What you think should happen instead?
At minimum, mask_secret() failures inside _get_connection() should not be allowed to abort connection retrieval / masquerade as the underlying job/task failing — masking is a logging side-effect and shouldn't be able to fail the primary operation. More fundamentally, SUPERVISOR_COMMS.send() (specifically the triggerer's comms decoder implementation) needs to correctly bridge calls made from within its own already-running event loop (e.g. via run_coroutine_threadsafe against the stored main loop) instead of raising when AsyncToSync detects it's already inside that loop.
How to reproduce
- Configure any secrets backend (e.g.
SecretsManagerBackend) with a Connection that has password and/or extra populated.
- Use that connection with a deferrable operator whose trigger re-fetches the connection during its
run() poll loop (e.g. DbtCloudRunJobOperator(deferrable=True), whose trigger calls provide_account_id's sync_to_async(self.get_connection)(...) on each status check).
- Trigger the DAG and let the task defer.
- Observe the triggerer log for the task: it fails within seconds with
RuntimeError: You cannot use AsyncToSync in the same thread as an async event loop, surfaced by the operator as a misleading DbtCloudJobRunException: Job run <id> has failed.
- Separately query the actual job status via the downstream API — it will still be in progress or will complete successfully well after Airflow reported failure.
Operating System
Amazon Linux 2 (MWAA managed runtime, Python 3.12)
Versions of Apache Airflow Providers
apache-airflow-providers-amazon==9.12.0
apache-airflow-providers-dbt-cloud==4.4.2
asgiref==3.9.1
(per the constraints-3.0.6/constraints-3.12.txt constraint file; MWAA pre-installs/version-locks these and does not allow pinning them independently.)
Deployment
Amazon Managed Workflows for Apache Airflow (MWAA)
Deployment details
MWAA environment running Airflow 3.0.6 on Python 3.12, with secrets.backend configured to SecretsManagerBackend. Same crash signature reproduced across three separate DAGs (each a single deferrable DbtCloudRunJobOperator task) within the same ~20 second window.
Anything else?
Happy to provide additional sanitized log excerpts if useful. I did not include specific connection IDs, hostnames, account identifiers, or credential values in this report.
Are you willing to submit PR?
Code of Conduct
Apache Airflow version
3.0.6
What happened?
Deferred
DbtCloudRunJobOperatortasks fail almost immediately with a misleadingDbtCloudJobRunException: Job run <id> has failed.even though the real dbt Cloud job is still running (and later completes successfully, sometimes hours later). The task fails within ~10 seconds of being deferred — far too fast to reflect a real terminal job state for a job that in fact took several more hours to finish.I traced this down to a real bug in Airflow core (Task SDK), not in the dbt Cloud provider itself, and not specific to any one secrets backend implementation. Full trace below.
Environment
secrets.backend:airflow.providers.amazon.aws.secrets.secrets_manager.SecretsManagerBackendsecrets.backend_kwargs:{"config_prefix":"airflow/config","connections_prefix":"airflow/connections","variables_prefix":"airflow/variables"}dbt_cloud(conn_typedbt_cloud) haspasswordpopulated (used by the provider as the dbt Cloud API token) and noextra.DbtCloudRunJobOperator(..., deferrable=True)using the connection above.Observed log sequence
Worker (synchronous
execute(), before deferring) — succeeds fine:Triggerer (async
DbtCloudRunJobTrigger.run()poll cycle) — crashes:This
TriggerEvent(status='error', ...)is then consumed byDbtCloudRunJobOperator.execute_complete(), which raisesDbtCloudJobRunException: Job run <job_run_id> has failed.— a message that implies the dbt Cloud job failed, when in reality Airflow never actually observed its terminal state at all.Confirmed independently via the dbt Cloud Admin API that the underlying run continued after Airflow marked the task failed, and later reached a real terminal state (
Success) several hours afterward — proving the "has failed" exception was not based on any actual observed job outcome.Root cause (traced against
apache/airflowtag3.0.6)This is not a dbt Cloud provider bug, and not specific to
SecretsManagerBackend. It's in_get_connection(),task-sdk/src/airflow/sdk/execution_time/context.py:secrets_backend.get_connection(conn_id)succeeds — the connection is fetched and deserialized fine.mask_secret(conn.password)is called next.mask_secret()(task-sdk/src/airflow/sdk/execution_time/secrets_masker.py:132-134) does:SUPERVISOR_COMMSis the triggerer's comms decoder (perairflow/jobs/triggerer_job_runner.py). Calling.send()on it from inside a coroutine that is already running the active event loop conflicts with the sync→async bridge (asgiref.sync.AsyncToSync) it uses internally, raising theRuntimeError.except Exceptionin the loop (logged as "Checking subsequent secrets backend"), but since only one backend is configured, the loop ends. Execution falls through to the unguarded fallback callSUPERVISOR_COMMS.send(GetConnection(conn_id=conn_id))at line 160 — the exact same.send()primitive, called from the exact same broken async context — which raises the identicalRuntimeError, this time uncaught, propagating up into the trigger'srun()coroutine and surfacing as a genericTriggerEvent(status='error').Key point: this has nothing to do with which secrets backend is configured, or which Connection field holds the secret. Any deferred operator that (a) uses a Connection with
passwordorextrapopulated, and (b) re-fetches that connection from inside the triggerer's asyncrun()loop (asDbtCloudRunJobTriggerdoes on every poll, viaprovide_account_id'ssync_to_async(self.get_connection)(...)call), will hit this. It happens to surface here viaapache-airflow-providers-dbt-cloud, but the defect is in Airflow core / Task SDK, not the provider.Related reports
RuntimeError: You cannot use AsyncToSync...signature, reported forBigQueryHookin a custom trigger (Airflow 3.0.3). Closed as "custom trigger code," but a later comment on that thread (2026-02-19) points out the identical crash occurs inside the officialDbtCloudRunJobTrigger.run()— this wasn't spun out into its own issue at the time.TriggerCommsDecoder.send()cross-loop conflict, this time through the amazon provider's async AWS hook. Closed by the reporter citing possible triggerer instability rather than a confirmed deterministic defect, due to lack of an isolated repro.DbtCloudRunJobOperator/DbtCloudJobRunSensor. Useful, but addresses a different symptom (malformed/unrecognized event payloads) and does not touch this root cause.This report differs from the above in that the trace above shows the exact same failure mode reproduced independently three times in the same ~20 second window (three separate deferred
DbtCloudRunJobOperatortasks polling around the same time), each with a matching stack trace ending insecrets_masker.py:134 mask_secret→triggerer_job_runner.py:740 send→asgiref/sync.py:186 AsyncToSync.__call__, and independently confirmed against the downstream API that the reported "job run has failed" was not based on a real observed terminal state for at least two of the three runs.What you think should happen instead?
At minimum,
mask_secret()failures inside_get_connection()should not be allowed to abort connection retrieval / masquerade as the underlying job/task failing — masking is a logging side-effect and shouldn't be able to fail the primary operation. More fundamentally,SUPERVISOR_COMMS.send()(specifically the triggerer's comms decoder implementation) needs to correctly bridge calls made from within its own already-running event loop (e.g. viarun_coroutine_threadsafeagainst the stored main loop) instead of raising whenAsyncToSyncdetects it's already inside that loop.How to reproduce
SecretsManagerBackend) with a Connection that haspasswordand/orextrapopulated.run()poll loop (e.g.DbtCloudRunJobOperator(deferrable=True), whose trigger callsprovide_account_id'ssync_to_async(self.get_connection)(...)on each status check).RuntimeError: You cannot use AsyncToSync in the same thread as an async event loop, surfaced by the operator as a misleadingDbtCloudJobRunException: Job run <id> has failed.Operating System
Amazon Linux 2 (MWAA managed runtime, Python 3.12)
Versions of Apache Airflow Providers
(per the
constraints-3.0.6/constraints-3.12.txtconstraint file; MWAA pre-installs/version-locks these and does not allow pinning them independently.)Deployment
Amazon Managed Workflows for Apache Airflow (MWAA)
Deployment details
MWAA environment running Airflow 3.0.6 on Python 3.12, with
secrets.backendconfigured toSecretsManagerBackend. Same crash signature reproduced across three separate DAGs (each a single deferrableDbtCloudRunJobOperatortask) within the same ~20 second window.Anything else?
Happy to provide additional sanitized log excerpts if useful. I did not include specific connection IDs, hostnames, account identifiers, or credential values in this report.
Are you willing to submit PR?
Code of Conduct