Runbook: Scanner — Scan Timeout on Complex Images
Use this runbook when scans of large or complex images fail to complete in time — jobs re-leased and eventually dead-lettered, queue visibility timeouts elapsing mid-scan, or a per-mechanism timeout (registry/HTTP forward, dotnet restore pre-step, verdict push) firing. It is written for the Platform on-call team.
There is no global scan-timeout setting in Stella Ops: timeouts are per-mechanism (queue lease, restore pre-step, matcher/verdict-push forwards), so this runbook walks through each real budget rather than a single knob. For related symptoms, see scanner-oom.mdand scanner-worker-stuck.md.
Reconciliation note (re-verified 2026-05-31; first reconciled 2026-05-30): This runbook was reconciled against
src/Scannerand the Doctor/CLI source. Several commands, config keys, the Doctor check, the alert, and the metric originally cited here did not exist in the codebase. Verified facts are grounded in source; constructs that are not implemented are marked NOT IMPLEMENTED with the closest real equivalent. The 2026-05-31 pass re-checked every cited config default, metric name, Doctor check ID, scope, and CLI verb against source (all confirmed) and corrected thestella image inspect/layersclaims, which the prior pass had backwards (the top-levelstella image inspectis fully implemented; onlystella scan image …is stubbed). SeeDecisions & Risksat the bottom. Ground truth files:
- Worker config:
src/Scanner/StellaOps.Scanner.Worker/Options/ScannerWorkerOptions.cs- WebService config:
src/Scanner/StellaOps.Scanner.WebService/Options/ScannerWebServiceOptions.cs- Worker metrics + Meter name:
src/Scanner/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerMetrics.cs,ScannerWorkerInstrumentation.cs- Scanner Doctor checks + plugin:
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Scanner/Checks/,ScannerDoctorPlugin.cs- Doctor category enum:
src/__Libraries/StellaOps.Doctor/Plugins/DoctorCategory.cs(Scanneris a valid--categoryvalue)- CLI verbs:
src/Cli/StellaOps.Cli/Commands/CommandFactory.cs,DoctorCommandGroup.cs,ImageCommandGroup.cs,CommandHandlers.Image.cs- Scan status / manifest REST:
src/Scanner/StellaOps.Scanner.WebService/Endpoints/ScanEndpoints.cs,ManifestEndpoints.cs- Scopes:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs
Metadata
| Field | Value |
|---|---|
| Component | Scanner |
| Severity | Medium |
| On-call scope | Platform team |
| Last updated | 2026-05-31 |
| Doctor check | check.scanner.queue, check.scanner.resources (no dedicated timeout-rate check exists) |
Symptoms
- [ ] Scan jobs fail and the worker logs a permanent failure (
scanner_worker_jobs_failed_totalincreases) - [ ] Jobs are re-leased repeatedly and eventually dead-lettered after
Scanner:Worker:Queue:MaxAttempts(default 5) attempts - [ ] Long jobs lose their lease: the queue visibility timeout (
scanner.Queue.VisibilityTimeoutSeconds, default 300s) elapses before the worker finishes, so the message is redelivered - [ ] Specific images consistently fail to complete
- [ ] Decompression guard tripped: error log mentions the extracted-bytes cap (
Pipeline.MaxExtractedBytes, default 8 GiB) - [ ]
dotnet restorepre-step aborts on the reachability stage afterReachability.DotNetRestoreTimeout(default 5m)
NOT IMPLEMENTED — verify before citing in an alert: There is no
scanner_scan_timeout_totalmetric and noScannerTimeoutExceededalert in the codebase. The scanner does not emit a single “scan operation exceeded timeout of X seconds” message from a global scan timeout — there is no global scan-timeout setting. Timeouts are per-mechanism (queue lease, restore pre-step, registry/HTTP forwards). Use the real metrics in the Verification section instead.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Specific images cannot be scanned; pipeline blocked |
| Data integrity | No data loss; jobs are idempotent and can be retried (see queue MaxDeliveryAttempts) |
| SLA impact | Release pipeline delayed for affected images |
Diagnosis
Quick checks
Run the scanner Doctor checks:
# Closest real checks to "is the scanner falling behind / failing": stella doctor --check check.scanner.queue stella doctor --check check.scanner.resources # List all available scanner checks: stella doctor list --category ScannerValid scanner Doctor check IDs (from
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Scanner/Checks/):check.scanner.queue,check.scanner.sbom,check.scanner.vuln,check.scanner.witness.graph,check.scanner.slice.cache,check.scanner.reachability,check.scanner.resources. There is nocheck.scanner.timeout-rate.--categoryis parsed to theDoctorCategoryenum, so the literal value isScanner(the valueScanner & Reachabilityyou see as the list heading is the plugin’s display name, not the filter token).Identify failing / stuck jobs. Job state lives in the
scannerschema (Postgres). There is NOstella scanner jobs listCLI command. Query the backend instead:# Job + run state are queried over the Scanner WebService REST surface: # GET /api/v1/scans/{scanId} (status) # GET /api/v1/scans/{scanId}/manifest (result manifest, when complete) # or inspect the scanner schema directly in Postgres.Look for: a pattern in image types or sizes among the failing scans.
Inspect the worker configuration in effect. There is NO
stella scanner config getcommand. Worker settings are bound from theScanner:Workerconfiguration section (env /../etc/scanner.yaml). Confirm the live values for:Scanner:Worker:MaxConcurrentJobs(default2)Scanner:Worker:Queue:MaxAttempts(default5)scanner:Queue:VisibilityTimeoutSeconds(WebService, default300)scanner:Queue:LeaseHeartbeatSeconds(WebService, default30)
Deep diagnosis
Analyze image complexity. The top-level
stella image inspectcommand is fully implemented — it pulls the real manifest viaIOciImageInspector, resolves a multi-arch index to platform manifests, and lists per-layer sizes and digests (ImageCommandGroup.BuildImageCommand→CommandHandlers.HandleInspectImageAsyncinsrc/Cli/StellaOps.Cli/Commands/CommandHandlers.Image.cs). The image reference is a positional argument (not a--refoption):# Real manifest pull; --print-layers lists each layer's size + digest: stella image inspect <image-ref> --print-layers --output json # Other flags: --resolve-index (default true), --platform linux/amd64, # --timeout 60 (seconds). Requires network (blocked in offline mode).Watch for very large or very many layers / files. (A second, stubbed image surface —
stella scan image inspect|layers --ref ...with hardcodedsha256:abc123...output,CommandFactory.csBuildScanImageCommand— also exists; do not use it for diagnosis, its output is placeholder.) For result data you can also use the scan manifest (GET /api/v1/scans/{scanId}/manifest).Check worker concurrency / load. There is NO
stella scanner workers statscommand. The worker exposes only:stella scanner workers get # shows configured worker countLive concurrency is bounded by
Scanner:Worker:MaxConcurrentJobs. For runtime utilization, use the OTLP metrics (see Verification) orstella doctor --check check.scanner.resources.Profile a scan. There is NO
stella scan image --image ... --profilecommand. The real ad-hoc scan entrypoint isstella scan run, which executes a scanner bundle against a directory target (not an image ref):stella scan run --runner docker --entry <scanner-image> --target <dir> --verboseTo see which stage is slow, read the per-stage duration metric
scanner_worker_stage_duration_ms(tagged by stage) rather than a CLI profiler flag.Check for filesystem-heavy images. There is no top-level
stella image layerscommand and no--sort-byoption anywhere. The top-levelstella imagegroup exposes onlyinspect. Useinspectwith--print-layersto get real per-layer sizes (sort the JSON yourself):stella image inspect <image-ref> --print-layers --output jsonA single layer with a very large uncompressed footprint can trip the
Pipeline.MaxExtractedBytesguard (default 8 GiB), which aborts extraction.
Resolution
Read first: Most “knobs” the previous version of this runbook listed (
timeouts.scan,scan.incremental_mode,cache.layer_dedup,cache.sbom_cache,sbom.streaming_threshold,sbom.file_sample_max,vuln.parallel_matching,vuln.match_workers) and the commands that set them (stella scanner config set,stella scanner workers restart,stella db optimize) do not exist in the codebase. The real, source-backed levers are below.
Immediate mitigation
Give a long job more time before its lease expires. Raise the queue visibility timeout so a slow scan is not redelivered mid-flight, and confirm the lease heartbeat is healthy:
# scanner WebService config (section: scanner) scanner: Queue: VisibilityTimeoutSeconds: 1200 # default 300 LeaseHeartbeatSeconds: 30 # default 30 MaxDeliveryAttempts: 5 # default 5Apply by updating the service configuration and restarting
scanner-web/scanner-worker(no in-process “restart workers” CLI exists).Reduce per-worker contention. If all worker slots are busy, lower concurrency per worker or run more worker replicas:
# scanner-worker config (section: Scanner:Worker) Scanner: Worker: MaxConcurrentJobs: 1 # default 2 — fewer concurrent jobs, more headroom eachRaise the extraction guard for legitimately large images. If a real (non-malicious) image exceeds the decompression-bomb cap:
Scanner: Worker: Pipeline: MaxExtractedBytes: 17179869184 # 16 GiB (default 8 GiB = 8589934592)
Root cause fix
If the SBOM->matcher forward to Concelier is timing out:
The worker forwards component PURLs to Concelier’s learn endpoint with a per-request budget (Scanner:Worker:Pipeline:Matcher:TimeoutSeconds, default 60). Raise it if Concelier is slow under load, or disable the forward to isolate the scan:
Scanner:
Worker:
Pipeline:
Matcher:
TimeoutSeconds: 120 # default 60
Enabled: true # set false to skip the forward entirely
If the .NET reachability restore pre-step is timing out:
The optional dotnet restore pre-step (only when Scanner:Worker:Reachability:DotNetExtractor is msbuild/restore/M1) is bounded by DotNetRestoreTimeout (default 5m). For large solutions, raise the budget and/or supply an offline package source:
Scanner:
Worker:
Reachability:
DotNetExtractor: auto # `auto` skips the restore pre-step entirely
DotNetRestoreTimeout: "00:10:00" # default 00:05:00
For air-gap restore, set
NUGET_OFFLINE_PACKAGES(see ADR-013 §M1).
If verdict push to an OCI registry is timing out:
Verdict push (when Scanner:Worker:VerdictPush:Enabled is true) has its own timeout and retry budget:
Scanner:
Worker:
VerdictPush:
Timeout: "00:10:00" # default 00:05:00
MaxRetries: 3 # default 3
Verification
# Re-run the scanner Doctor checks and confirm no failures:
stella doctor --check check.scanner.queue
stella doctor --check check.scanner.resources
# Re-run an ad-hoc scan (directory target via a scanner bundle):
stella scan run --runner docker --entry <scanner-image> --target <dir> --verbose
Confirm the real OTLP metrics (Meter StellaOps.Scanner.Worker, see ScannerWorkerMetrics.cs) are healthy:
scanner_worker_jobs_completed_total— increasing (jobs finishing)scanner_worker_jobs_failed_total— flat (no new permanent failures)scanner_worker_job_duration_ms— total processing duration per jobscanner_worker_stage_duration_ms— per-stage duration (find the slow stage)scanner_worker_queue_latency_ms— enqueue-to-lease latency (backlog pressure)
There is no
scanner_scan_timeout_totalmetric to watch.
Prevention
- [ ] Lease budget: Size
scanner.Queue.VisibilityTimeoutSeconds(default 300s) to comfortably exceed the p99 ofscanner_worker_job_duration_msso long scans are not redelivered mid-flight. - [ ] Capacity: Tune
Scanner:Worker:MaxConcurrentJobs(default 2) and worker replica count against observedscanner_worker_queue_latency_ms. - [ ] Monitoring: Alert on a rising
rate(scanner_worker_jobs_failed_total)and onscanner_worker_queue_latency_msp99 growth. (No off-the-shelfScannerTimeoutExceededalert ships today — author the alert rule against these real series.) - [ ] Extraction guard: Keep
Pipeline.MaxExtractedBytes(default 8 GiB) as a decompression-bomb guard; only raise it for known-large legitimate images. - [ ] Documentation: Document image size/complexity limits in the user guide.
Related Resources
- Architecture:
docs/modules/scanner/architecture.md - Related runbooks:
scanner-oom.md,scanner-worker-stuck.mdNote:
scanner-worker-stuck.mdmay still cite some of the non-existent CLI vocabulary reconciled out of this file; verify its commands against source before relying on them. - Dashboard: Grafana > Stella Ops > Scanner Performance
Decisions & Risks
- Doctor check corrected.
check.scanner.timeout-ratedoes not exist. The Scanner Doctor plugin (ScannerDoctorPlugin.cs) registers exactly:check.scanner.queue,check.scanner.sbom,check.scanner.vuln,check.scanner.witness.graph,check.scanner.slice.cache,check.scanner.reachability,check.scanner.resources. - Metric corrected.
scanner_scan_timeout_totaldoes not exist. Real worker metrics are prefixedscanner_worker_*(seeScannerWorkerMetrics.cs). - Alert flagged.
ScannerTimeoutExceededis not defined anywhere in the repo; it must be authored as a Prometheus rule against the real series above. - CLI commands corrected/removed.
stella scan image,--image,--timeout,--profile,--fast-mode,--retry,--profile-memory;stella scanner jobs *,stella scanner config *,stella scanner workers stats|restart|add,stella scanner logs,stella db optimize— none of these exist. Real verbs:stella scan run,stella scanner download,stella scanner workers get|set --count,stella image inspect <ref> --print-layers(real manifest pull viaIOciImageInspector— NOT a stub; positionalreferencearg, not--ref; there is no top-levelstella image layers), andstella doctor(--check,list,run,export,fix).- Correction to prior reconciliation (this pass): the 2026-05-30 note that
stella image inspect/layers“emit stubbed output today” was wrong — it conflated the top-levelstella image inspect(ImageCommandGroup.BuildImageCommand, fully implemented) with the placeholderstella scan image inspect|layers --ref ...(CommandFactory.csBuildScanImageCommand, which IS stubbed). Use the top-level command for diagnosis.
- Correction to prior reconciliation (this pass): the 2026-05-30 note that
- Config keys corrected. There is no global
timeouts.scanand noscan.incremental_mode/cache.*/sbom.streaming_threshold/sbom.file_sample_max/vuln.parallel_matching/vuln.match_workersbinding. Real timeout/limit knobs live underScanner:Worker:*(MaxConcurrentJobs,Queue:MaxAttempts,Pipeline:MaxExtractedBytes,Pipeline:Matcher:TimeoutSeconds,Reachability:DotNetRestoreTimeout,VerdictPush:Timeout,Shutdown:Timeout) andscanner:Queue:*(VisibilityTimeoutSeconds,LeaseHeartbeatSeconds,MaxDeliveryAttempts), plusscanner:Storage:CommandTimeoutSecondsandscanner:ArtifactStore:TimeoutSeconds. - Scopes. Scan operations require
scanner:scan; reading results requiresscanner:read; config writes requirescanner:write(StellaOpsScopes.cs).
