Skip to content

Use one union blob per warm Python run

Status Date
accepted 2026-09-20

The Python path must run ansible-core modules on a managed host without installing ansible-core there. A process per task costs 340 ms. Forking from a Python server that has loaded nothing costs 266 ms, which is not enough to justify the machinery.

Loading module_utils from one module’s zip and then forking costs 12.8 ms, but it pins Python’s ansible package to that one zip. Every other module then fails to import. This was measured, and it fails loudly rather than silently.

The entries shared between different module zips are byte-identical: zero conflicts across 146 entries from ten modules. Merging them into one zip is well defined, and the builder treats a conflict as a hard error.

Build one union blob per run, not one blob per module. The blob contains every module and its imported module_utils, is identified by the BLAKE3 hash of its contents, and is cached on the managed host. The Python server loads it once and forks a child for each task.

The union blob for the five golden modules is exactly 631050 bytes, compared with 2307982 bytes for separate zips. That is a 72.7 percent saving. The preloaded fork path costs 12.8 ms per task, and loading the blob costs 264.5 ms once per server.

  • A run gets one consistent module_utils set, so modules do not import from incompatible zips.
  • The builder must stop on a conflicting shared entry rather than choose one version.
  • The managed host caches and verifies the blob before using it.
  • The controller needs ansible-core; the managed host does not.

The numbers above come from single modules in a loop. A thirty-five task playbook against one managed host, run on 2026-09-21, says what they are worth to a real play. It installs packages, creates directories, writes a configuration line by line, reads files back and holds a service started, and it escalates, because that is what such a play does.

Three passes each against a host the play had already converged on, so every pass does the same work, release build. The median, as first measured:

First measurementThree passes against a converged host, release build, median. Shorter is faster.
  • Volant30.20 sVolant30.20 s863 ms per task
  • ansible-core, no pipelining27.69 sansible-core, no pipelining27.69 s791 ms per task
  • ansible-core, pipelining20.02 sansible-core, pipelining20.02 s572 ms per task
Show as a table
First measurement
Volant30.20 s
ansible-core, no pipelining27.69 s
ansible-core, pipelining20.02 s

Volant now runs the play in 40 percent of the pipelined reference’s time. A pass opens 3 ssh sessions on the host instead of 58. The before arm reproduces the first table within one percent, so the gain comes from the fix rather than from a change in the machine.

The per-task average still includes real work. apt spends about 755 ms talking to dpkg, and gathering facts is the most expensive task in the play. The warm path removes the overhead around a module, not the module’s own work.

Keeping the link costs something. While the permit carried the link with it, forks also bounded how many escalated connections a run held open. Now a run can hold two links per host, so at three file descriptors a link, a controller limited to 1024 descriptors reaches its ceiling at about 169 escalating hosts rather than about 330.