# ansible-lint speed improvement research

**URL:** <https://forum.ansible.com/t/ansible-lint-speed-improvement-research/46045>\
**Category:** Project Discussions\
**Tags:** ansible-lint\
**Created:** [July 14, 2026, 6:07am UTC](https://forum.ansible.com/t/ansible-lint-speed-improvement-research/46045 "2026-07-14T06:07:24Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![bendem](https://avatars.discourse-cdn.com/v4/letter/b/b782af/32.png) [@bendem](https://forum.ansible.com/u/bendem)\
**Post date:** [July 14, 2026, 6:07am UTC](https://forum.ansible.com/t/ansible-lint-speed-improvement-research/46045/1 "2026-07-14T06:07:25Z")

</div>

The slowest part of our CI is ansible-lint, we have a modest ansible repo (36 roles, 265 playbooks), the ansible-lint execution on our CI takes between 7 and 12 minutes.

I got my hands on a Claude subscription and I was able to get it to identify two bottlenecks that improved runtime on our repo by about 40% (7m12 → 4m16s).

First one is `get_deps_versions` getting called for every template render. Adding `@functools.cache` drops 947 calls in our repo.

Second one is ansible-playbook --syntax-check invocation. Rewritting runner to call `PlaybookCLI.cli_executor` instead of spawning a subprocess while serializing calls to avoid messing ansible’s global state since it’s not thread safe. This is the most major improvement: faster, lighter (28% lower cpu time).

I’m curious if others can reproduce those results of if it’s just very specific to us.

Here is a sitecustomize that can be dropped in your python path to check those findings.

```py
import contextlib
import functools
import importlib.abc
import importlib.util
import io
import os
import subprocess
import sys
import threading
import warnings

_syntax_check_lock = threading.Lock()

class _CompletedInProcess:
    def __init__ (self, returncode, stdout, stderr):
        self.returncode = returncode
        self.stdout = stdout
        self.stderr = stderr

def _run_syntax_check_in_process(cmd, env):
    from ansible.cli.playbook import PlaybookCLI

    stdout = io.StringIO()
    stderr = io.StringIO()
    with _syntax_check_lock:
        overrides = {k: v for k, v in env.items() if os.environ.get(k) != v}
        previous = {k: os.environ.get(k) for k in overrides}
        os.environ.update(overrides)
        try:
            with (
                warnings.catch_warnings(),
                contextlib.redirect_stdout(stdout),
                contextlib.redirect_stderr(stderr),
            ):
                warnings.simplefilter("ignore")
                try:
                    PlaybookCLI.cli_executor(list(cmd))
                    returncode = 0
                except SystemExit as exc:
                    returncode = exc.code if isinstance(exc.code, int) else 1
        finally:
            for key, value in previous.items():
                if value is None:
                    os.environ.pop(key, None)
                else:
                    os.environ[key] = value
    return _CompletedInProcess(returncode, stdout.getvalue(), stderr.getvalue())

class _SyntaxCheckSubprocess:
    @staticmethod
    def run(cmd, *args, env=None, **kwargs):
        if (
            isinstance(cmd, (list, tuple))
            and len(cmd) >= 2
            and cmd[0] == "ansible-playbook"
            and "--syntax-check" in cmd
        ):
            return _run_syntax_check_in_process(cmd, env or {})
        return subprocess.run(cmd, *args, env=env, **kwargs)

    def __getattr__ (self, name):
        return getattr(subprocess, name)

class _RunnerPatcher(importlib.abc.MetaPathFinder):
    _target = "ansiblelint.runner"

    def find_spec(self, fullname, path, target=None):
        if fullname != self._target:
            return None
        sys.meta_path.remove(self)
        try:
            spec = importlib.util.find_spec(fullname)
        finally:
            sys.meta_path.insert(0, self)
        if spec is None or spec.loader is None:
            return None
        original_exec_module = spec.loader.exec_module

        def exec_module(module):
            original_exec_module(module)
            # actual patching happens here
            module.subprocess = _SyntaxCheckSubprocess()
            with contextlib.suppress(ValueError):
                sys.meta_path.remove(self)

        spec.loader.exec_module = exec_module
        return spec

if not any(isinstance(f, _RunnerPatcher) for f in sys.meta_path):
    sys.meta_path.insert(0, _RunnerPatcher())

class _DepsVersionsCachePatcher(importlib.abc.MetaPathFinder):
    _target = "ansiblelint.config"

    def find_spec(self, fullname, path, target=None):
        if fullname != self._target:
            return None
        sys.meta_path.remove(self)
        try:
            spec = importlib.util.find_spec(fullname)
        finally:
            sys.meta_path.insert(0, self)
        if spec is None or spec.loader is None:
            return None
        original_exec_module = spec.loader.exec_module

        def exec_module(module):
            original_exec_module(module)
            # actual patching happens here
            module.get_deps_versions = functools.cache(module.get_deps_versions)
            with contextlib.suppress(ValueError):
                sys.meta_path.remove(self)

        spec.loader.exec_module = exec_module
        return spec

if not any(isinstance(f, _DepsVersionsCachePatcher) for f in sys.meta_path):
    sys.meta_path.insert(0, _DepsVersionsCachePatcher())

```

---

<div class="post-metadata">

**Author:** ![sivel](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/sivel/32/1198_2.png) [@sivel](https://forum.ansible.com/u/sivel)\
**Post date:** [July 14, 2026, 3:39pm UTC](https://forum.ansible.com/t/ansible-lint-speed-improvement-research/46045/2 "2026-07-14T15:39:19Z")

</div>

`PlaybookCLI.cli_executor` is not a public API. ansible-core does not support, nor expose, any public Python APIs related to executing playbooks. As it is not a public API, it may change at any time, in backwards incompatible ways, without any notification. The only public API for executing a playbook is invoking the CLI.

---

<div class="post-metadata">

**Author:** ![utoddl](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/utoddl/32/249_2.png) [@utoddl](https://forum.ansible.com/u/utoddl)\
**Post date:** [July 14, 2026, 8:08pm UTC](https://forum.ansible.com/t/ansible-lint-speed-improvement-research/46045/3 "2026-07-14T20:08:09Z")

</div>

> [@sivel](#):
>
> …not a public API…

You’re absolutely correct to point this out. No way should _core_ even hint at this being the kind of chainsaw to learn to juggle with.

Still, for a point-in-time what-if personal just-for-science project — Wow! That is a pretty impressive way to void your warranty! Kudos to @bendem for the best implemented bad idea I’ve seen this year. 🤠

---

<div class="post-metadata">

**Author:** ![bendem](https://avatars.discourse-cdn.com/v4/letter/b/b782af/32.png) [@bendem](https://forum.ansible.com/u/bendem)\
**Post date:** [July 14, 2026, 8:34pm UTC](https://forum.ansible.com/t/ansible-lint-speed-improvement-research/46045/4 "2026-07-14T20:34:58Z")

</div>

Ah, I knew there had to be a reason. It’s really too bad because that’s a really big performance improvement. Serial execution is much faster than parallel execution on 4 CPUs.
