Concurrency and performance¶
Lintel is designed to fit inside a developer's pre-commit wait budget - ideally under 2 seconds for a small staged change, under 10 for a large one. This page describes the knobs.
Target budgets¶
| Invocation | Repo size | Target wall time |
|---|---|---|
pre-commit (1–5 files) |
any | < 2 s |
pre-commit (50 files) |
medium | < 10 s |
pre-push |
commits being pushed | < 30 s |
| CI (full working tree) | any | bounded by timeouts.total |
These are targets, not guarantees. A full dependency scan (osv-scanner) can dominate on npm monorepos with thousands of transitive dependencies.
Parallelism¶
Each enabled check × stack spawns a goroutine. Up to concurrency.max_parallel run at once (defaults to runtime.NumCPU()).
concurrency:
max_parallel: 4 # explicit cap; set to 1 for deterministic CI logs
scanner_nice: true # on Unix, renice scanners to 10 (lower priority than your shell)
- Independent. Scanners never share files on disk or in memory. They read the staged file list and emit findings into a per-scanner buffer.
- Bounded. No scanner can spawn more workers than the cap, even when the scanner's own
--jobsflag is higher. - Fair. A slow scanner does not starve fast ones; they all start in the same wave.
Timeouts¶
timeouts:
per_check: 30s # kill any single scanner that exceeds this
total: 120s # kill everything if the whole run exceeds this
A timed-out scanner yields exit 4 for that check but does not fail the other checks. The gate still runs on the findings that did complete - so a timeout does not silently pass the gate.
Staged-file scoping¶
On a Git hook, Lintel passes only the staged file list to scanners that accept a file argument. For scanners that always scan the whole tree (for example, osv-scanner on package-lock.json), the file list is still computed so that the filter stage knows which findings originate in changed files.
This is the single largest performance win over "just run gitleaks" approaches: a 20-file commit in a 200k-file repo still finishes in seconds.
Measured cost¶
lintel run --verbose prints per-scanner elapsed times. A typical breakdown on a Go service with a ~1k-line pre-commit change:
Total: ~1.3 s wall, since golangci-lint and osv-scanner overlap.
Tuning tips¶
- Move
dependenciestopre-push. Lockfiles change less often than code. Ahooks.pre_commit.checkslist that omitsdependencies, combined withhooks.pre_push.checks: [dependencies], keeps pre-commit snappy. - Exclude
vendor/,node_modules/, generated code.paths.excludeis cheap - it happens before any scanner runs. - Set
max_parallel: 2on CI runners with shared CPU. More parallelism is not always faster under contention. - Inspect
--verboseoutput before hypothesizing. Most "slow" reports turn out to be one specific scanner on one specific file.
What Lintel does not do¶
- It does not cache scanner results between runs. A scanner re-reads the file on each invocation. Caching is a v2.0 roadmap item - until then, correctness wins over cleverness.
- It does not do incremental analysis. A file is either in scope or not; there is no "diff of findings" calculation at the scanner level.