Skip to main content

Recipe & plugin performance

How to make honey cue-exec runs and docker plugins faster. Most of the heavy machinery is already pooled and reused per run — the wins below are the levers you control, ordered by impact.

0. Measure first​

Don't guess where the time goes — measure, then change one thing.

honey cue-exec --profile <recipe.cue> "<query>" # CPU/Mem/Network/SSH stats
honey cue-exec --debug-log /dev/stderr <recipe.cue> "<query>" # per-step timings

--profile prints per-run CPU, memory, network and SSH stats. --debug-log shows per-step "tunnel starting/ready", plugin create/ready, and per-host timing. If a collector is wired, OTEL spans are emitted per step.

1. Run independent steps in parallel — type: "graph"​

The default (linear) mode runs steps one at a time, even when they don't depend on each other. Switch to graph mode and declare dependencies; steps with no dependency between them run concurrently.

recipe: {
name: "example"
type: "graph" // <- opt into parallel steps
steps: [
{ id: "a", host: "*", command: "..." },
{ id: "b", host: "*", command: "..." }, // a and b run together
{ id: "c", host: "*", depends: ["a", "b"], command: "..." }, // c waits for both
]
}

Graph mode is a true dataflow scheduler: each step starts the instant its own depends finish — a slow step never holds up an unrelated peer, and a step two levels deep runs as soon as its chain is ready rather than waiting for the slowest step at its level. By default up to 8 steps run concurrently; raise it with defaults.max_parallel (see §2), which also sizes the graph worker pool. There is no per-level barrier, so you get parallelism from declaring dependencies accurately — over-declaring depends serializes work that could overlap.

2. Raise host fan-out — max_parallel​

Within a step, hosts run concurrently. When max_parallel is unset, the default depends on the step kind: 8 for plugin / tunnel / template / k8s steps, 32 for command / script. For many hosts, raise it:

recipe: {
defaults: { max_parallel: 32 } // applies to every step kind (1-128)
...
}

Or per step (max_parallel: N on the step). To raise it for all recipes without editing each one, set it once in the honey config:

# honey.yaml
defaults:
max_parallel: 32 # seeds the recipe default when a recipe doesn't set one

Per-recipe / per-step max_parallel always wins over the config default. serial: true on a step forces one-host-at-a-time (only when required).

Heavier step kinds (remote docker plugins) create a container per host — raising concurrency multiplies containers, so size max_parallel to what the target and the operator can handle.

3. Share tunnels and KV sessions​

A tunnel step without share_key opens one tunnel per host. Add a share_key so every host/step reuses a single operator-side listener:

{ id: "pg", host: "db-*", tunnel: { mode: "local", remote_host: "localhost", remote_port: 5432, share_key: "pg" } }

Use recipe-scoped kv_tunnel (defaults.kv_tunnel) rather than per-step, so the stepkv forward is opened once for the run instead of per host per step.

4. Keep secrets cheap in hot loops​

Secret refs are resolved once per run and cached (per-run memoization), so the same ref reused across many hosts/steps costs a single backend call. Still, prefer local backends (secure:, age:, env:) for values reused widely — network backends (aws-kms:, aws-sm:, vault:, k8s:) pay a round-trip on the first resolve. Seal once into a secure: ref and reference that, rather than a live KMS lookup per distinct ref.

5. Docker plugins: keep the container warm, avoid per-host multiplication​

  • The container is long-lived. The first plugin call pays create + start + readiness; every later call is a cheap HTTP call. Running under honey web (a persistent process) keeps the container warm across requests; a fresh honey CLI invocation pays cold start again.
  • Reuse the container across CLI runs — plugins.keep_warm. Set plugins.keep_warm: true (default off) and a docker plugin's container is created with a deterministic name + labels and left running on Close, so the next honey run attaches to it instead of paying cold start. A container is reused only when its identity (image, entrypoint, env, volumes, network) matches; a stale build whose shim api_version no longer matches is replaced automatically. Warm containers linger until you reap them: honey plugins gc removes them all, or honey plugins gc --older-than 1h removes only those idle-by-age. The tradeoff is lingering containers vs. repeated cold starts — enable it when you run the same plugins repeatedly from the CLI.
  • Remote plugin runs create one container per host — cost scales with host count. When the plugin just needs to reach a service (e.g. pghero → Postgres), run it once on the operator (host: "_") against a tunneled endpoint instead of fanning the container out to every host. See pghero_tunnel_demo.cue and pghero_unix_tunnel.cue.
  • Keep pull_policy: if_not_present (the default) — always re-pulls on every container start. Pre-build/pre-pull the image once.
  • Prefer a WASM plugin when the work allows it — WASM runs in-process (no container, no startup), so it beats docker for pure compute/logic. Use docker only when you need the container's tools/runtime (ruby / psql / trivy / …).

6. Already free (don't reinvent)​

Per run, honey already: pools and reuses SSH connections across all steps/hosts; builds the plugin manager and docker host session once; gathers facts once; and refcounts tunnels in a shared pool. So the levers above (graph mode, max_parallel, share_key / KV scope, secret backend choice, warm / WASM plugins) are where your tuning pays off.

See also: CUE recipes, Plugin development.