TL;DR:
The core concept is to use a pre-commit hook to precompute and serialize the results of get_cli_commands into _generated_commands.py.
After conducting some benchmarking ( check last part of issue ) and a proof of concept (POC), I found that the Airflow CLI can be sped up by 3 to 4 times for non-custom AuthManagers and Executors.
Why
While exploring airflow/cli/cli_parser.py, I noticed that get_auth_manager_cls is imported from airflow.api_fastapi.app. Since FastAPI is unrelated to the CLI, I investigated further.
The primary reasons why the current CLI is slow:
|
from airflow.api_fastapi.app import get_auth_manager_cls |
|
from airflow.cli.cli_config import ( |
|
DAG_CLI_DICT, |
|
ActionCommand, |
|
DefaultHelpParser, |
|
GroupCommand, |
|
core_commands, |
|
) |
|
from airflow.cli.utils import CliConflictError |
|
from airflow.exceptions import AirflowException |
|
from airflow.executors.executor_loader import ExecutorLoader |
|
from airflow.utils.helpers import partition |
|
|
|
if TYPE_CHECKING: |
|
from airflow.cli.cli_config import ( |
|
Arg, |
|
CLICommand, |
|
) |
|
|
|
airflow_commands = core_commands.copy() # make a copy to prevent bad interactions in tests |
|
|
|
log = logging.getLogger(__name__) |
|
|
|
|
|
for executor_name in ExecutorLoader.get_executor_names(): |
|
try: |
|
executor, _ = ExecutorLoader.import_executor_cls(executor_name) |
|
airflow_commands.extend(executor.get_cli_commands()) |
|
except Exception: |
|
log.exception("Failed to load CLI commands from executor: %s", executor_name) |
|
log.error( |
|
"Ensure all dependencies are met and try again. If using a Celery based executor install " |
|
"a 3.3.0+ version of the Celery provider. If using a Kubernetes executor, install a " |
|
"7.4.0+ version of the CNCF provider" |
|
) |
|
# Do not re-raise the exception since we want the CLI to still function for |
|
# other commands. |
|
|
|
try: |
|
auth_mgr = get_auth_manager_cls() |
|
airflow_commands.extend(auth_mgr.get_cli_commands()) |
|
except Exception as e: |
|
log.warning("cannot load CLI commands from auth manager: %s", e) |
|
log.warning("Authentication manager is not configured and webserver will not be able to start.") |
|
# do not re-raise for the same reason as above |
|
if len(sys.argv) > 1 and sys.argv[1] == "webserver": |
|
log.exception(e) |
|
sys.exit(1) |
|
|
- It loads the entire executor module.
- It loads the entire auth manager module.
Although these modules are dynamically loaded using import_string, there is still significant overhead when loading CeleryKubernetesExecutor, as it requires the celery and kubernetes packages.
How
Instead of fully decoupling the CLI interface from AuthManagers and Executors—by separating get_cli_commands from Base<AuthManager/Executor> interfaces, which could introduce breaking changes and require significant migration effort.
By adding a pre-commit hook to collect all CLI commands and store them precomputed and statically in airflow.cli.commands._generated_commands.py which can significantly speed up CLI response times.
Note:
For custom AuthManagers or Executors, the original approach will still be used, meaning get_cli_commands will be dynamically loaded and called as before.
Steps
Step 3 is the key focus.
- Decouple
auth_manager from heavy dependencies like FastAPI and Flask by only importing them within methods.
Since the auth_manager implementation itself does not import FastAPI directly (only uses it for type hints), this ensures lighter module loading.
- Introduce
auth_manager_constants.py as a single source of truth for available AuthManagers, similar to how executor_constants.py works for Executors.
- Add a pre-commit hook to generate
EXECUTORS_CLI_COMMANDS and AUTH_MANAGERS_CLI_COMMANDS by precomputing CLI commands for all supported Executors and AuthManagers.
What
This change is expected to speed up the CLI by 3–4× for non-custom AuthManagers and Executors.
It could be a significant performance improvement in either Airflow 3.0 or a 2.10.x release!
Details of the Pre-Commit Script
POC branch
Example of _generated_commands.py
EXECUTORS_CLI_COMMANDS = {
"KubernetesExecutor": [
GroupCommand(
name="kubernetes",
help="Tools to help run the KubernetesExecutor",
subcommands=[
ActionCommand(
name="cleanup-pods",
help="Clean up Kubernetes pods (created by KubernetesExecutor/KubernetesPodOperator) in evicted/failed/succeeded/pending states",
func=lazy_load_command(
"airflow.providers.cncf.kubernetes.cli.kubernetes_command.cleanup_pods"
),
args=[
Arg(
flags=("--namespace",),
help="Kubernetes Namespace. Default value is `[kubernetes] namespace` in configuration.",
default=conf.get("kubernetes_executor", "namespace"),
),
Arg(
flags=("--min-pending-minutes",),
help="Pending pods created before the time interval are to be cleaned up, measured in minutes. Default value is 30(m). The minimum value is 5(m).",
default=30,
type=positive_int(allow_zero=False),
),
Arg(
flags=("-v", "--verbose"),
help="Make logging output more verbose",
action="store_true",
),
],
description=None,
epilog=None,
hide=False,
),
# ...
Key Considerations for Serialization
Benchmark
MacOS, M2 Chip, 16G RAB, In breeze container.
Terminate breeze container and start a new session every benchmark.
breeze shell --answer n
/files/timer.sh airflow --help
benchmark script: timer.sh
With LocalExecutor
| Configuration |
Run 1 |
Run 2 |
Run 3 |
Run 4 |
Run 5 |
Average |
| Original |
3.0536 |
3.0363 |
3.1202 |
3.2155 |
3.1527 |
3.1157 |
Remove ExecutorLoader |
3.5628 |
3.0111 |
3.0560 |
3.0268 |
3.2067 |
3.1727 |
Remove get_auth_manager_cls |
1.1342 |
0.9723 |
1.1694 |
1.1146 |
0.9813 |
1.0744 |
Remove ExecutorLoader & get_auth_manager_cls |
1.0761 |
1.0334 |
0.9444 |
0.9392 |
0.9404 |
0.9867 |
| My POC |
1.1256 |
0.9538 |
0.9181 |
0.9486 |
0.9374 |
0.9767 |
3.1157 / 0.9767 = 3.19002764, 3.19x faster !
With CeleryKubernetesExecutor
| Configuration |
Run 1 |
Run 2 |
Run 3 |
Run 4 |
Run 5 |
Average |
| Original |
4.0599 |
3.9483 |
3.7732 |
3.5978 |
3.6112 |
3.7980 |
Remove ExecutorLoader |
3.1474 |
2.8519 |
2.8384 |
2.8141 |
2.7976 |
2.8899 |
Remove get_auth_manager_cls |
2.0906 |
1.7065 |
1.7159 |
1.7245 |
1.7101 |
1.7895 |
Remove ExecutorLoader & get_auth_manager_cls |
1.1131 |
0.9080 |
0.9181 |
0.9265 |
0.9177 |
0.9567 |
| My POC |
1.0236 |
0.9300 |
0.9308 |
0.9221 |
0.9177 |
0.9448 |
3.7980 / 0.9448 = 4.01989839, 4x faster !
Related issues
No response
Are you willing to submit a PR?
Code of Conduct
TL;DR:
The core concept is to use a pre-commit hook to precompute and serialize the results of
get_cli_commandsinto_generated_commands.py.After conducting some benchmarking ( check last part of issue ) and a proof of concept (POC), I found that the Airflow CLI can be sped up by 3 to 4 times for non-custom AuthManagers and Executors.
Why
While exploring
airflow/cli/cli_parser.py, I noticed thatget_auth_manager_clsis imported fromairflow.api_fastapi.app. Since FastAPI is unrelated to the CLI, I investigated further.The primary reasons why the current CLI is slow:
airflow/airflow/cli/cli_parser.py
Lines 39 to 87 in 4c031cb
How
Instead of fully decoupling the CLI interface from AuthManagers and Executors—by separating
get_cli_commandsfromBase<AuthManager/Executor>interfaces, which could introduce breaking changes and require significant migration effort.By adding a pre-commit hook to collect all CLI commands and store them precomputed and statically in
airflow.cli.commands._generated_commands.pywhich can significantly speed up CLI response times.Note:
For custom AuthManagers or Executors, the original approach will still be used, meaning
get_cli_commandswill be dynamically loaded and called as before.Steps
auth_managerfrom heavy dependencies likeFastAPIandFlaskby only importing them within methods.auth_manager_constants.pyas a single source of truth for available AuthManagers, similar to howexecutor_constants.pyworks for Executors.EXECUTORS_CLI_COMMANDSandAUTH_MANAGERS_CLI_COMMANDSby precomputing CLI commands for all supported Executors and AuthManagers.What
This change is expected to speed up the CLI by 3–4× for non-custom AuthManagers and Executors.
It could be a significant performance improvement in either Airflow 3.0 or a 2.10.x release!
Details of the Pre-Commit Script
POC branch
Example of
_generated_commands.pyKey Considerations for Serialization
The
func: Callablefield inActionCommandlazy_load_commandis called for thefuncfield.The
type: type | Callablefield inArgtypefield can be a built-in type such asint,str, orbool, or a callable likepositive_intfromairflow.cli.cli_configorparsefromairflow.utils.timezone.__module__attribute and mapping it to the correct callable.The
defaultfield inArgdefaultfield can either be a hardcoded value or retrieved usingconf.get*(section, key).conf.get,conf.getboolean, orconf.getint, it should be serialized as aconf.get*string instead of a precomputed result.airflow.cfgorAIRFLOW__*environment variables.conf.get*methods are monkey-patched by adding a decorator that returns a wrapper type with a custom__conf_source__attribute.__conf_source__, determine whether to serialize it as aconf.get*string or a hardcoded constant.Benchmark
MacOS, M2 Chip, 16G RAB, In breeze container.
Terminate breeze container and start a new session every benchmark.
benchmark script: timer.sh
With
LocalExecutorExecutorLoaderget_auth_manager_clsExecutorLoader&get_auth_manager_cls3.1157 / 0.9767 = 3.19002764, 3.19x faster !
With
CeleryKubernetesExecutorExecutorLoaderget_auth_manager_clsExecutorLoader&get_auth_manager_cls3.7980 / 0.9448 = 4.01989839, 4x faster !
Related issues
No response
Are you willing to submit a PR?
Code of Conduct