Runbook: Scanner — Out of Memory on Large Images
Use this runbook when the Stella Ops scanner worker is killed or fails while scanning large container images — typically an OS OOM-kill (exit 137) or a .NET OutOfMemoryException during layer extraction or analysis. It is written for the Platform on-call team and focuses on the two real memory-bounding knobs (Scanner:Worker:Pipeline:MaxExtractedBytes and Scanner:Worker:MaxConcurrentJobs) plus the container memory limit.
For related symptoms, see scanner-timeout.md, scanner-worker-stuck.md, and scanner-sbom-generation-failed.md.
Metadata
| Field | Value |
|---|---|
| Component | Scanner (worker — StellaOps.Scanner.Worker, .NET 10) |
| Severity | High |
| On-call scope | Platform team |
| Last updated | 2026-05-31 |
| Doctor check | check.scanner.resources (ScannerResourceUtilizationCheck) |
Reconciliation note (2026-05-31, second pass): This runbook was reconciled against
src/Scannerandsrc/Doctorand re-verified line-by-line on 2026-05-31. The scanner worker is a .NET 10 service (StellaOps.Scanner.Worker.csprojtargetsnet10.0), not a Go binary, so OOM surfaces as an OS OOM-kill (exit 137) or a .NETOutOfMemoryException— not a Go runtime panic. Several CLI verbs and config keys in the original draft (stella scanner config,stella scanner jobs,stella scanner logs,stella scanner scale,stella scanner workers scale,worker.memory_limit,sbom.streaming_threshold,sbom.incremental_mode,cache.layer_dedup,routing.*) do not exist in source and have been removed or marked NOT IMPLEMENTED — the onlyscannersubcommands aredownload,workers get/set,secrets, andplugins. The real memory-bounding knobs areScanner:Worker:Pipeline:MaxExtractedBytes(default 8 GiB, verified inScannerWorkerOptions.ScanPipelineOptions) andScanner:Worker:MaxConcurrentJobs(default 2). Thecheck.scanner.resourcesDoctor probe and its memory thresholds (Fail ≥95%, Warn ≥80%), the worker metric names, and thescanner:read/scan/write/exportscopes were all confirmed against source.
Symptoms
- [ ] Scanner worker container exits with code 137 (OS OOM-killed). Exit 137 is a Linux SIGKILL from the cgroup/host OOM killer and is independent of runtime.
- [ ] Scans fail consistently for specific large images
- [ ] Worker log contains a .NET
System.OutOfMemoryExceptionduring a stage (most often thebuild-filesystemlayer-extraction stage or an analyzer stage; the stage value emitted in logs/metric tags is the hyphenatedbuild-filesystem, from theScanStageNames.BuildFilesystemconstant) - [ ] Layer extraction aborts with the
LayerArchiveExtractordecompression-bomb guard — anInvalidOperationExceptionwhose message isbuild-filesystem: extraction exceeded the <N>-byte limit.(theScanner:Worker:Pipeline:MaxExtractedBytescap, default 8 GiB — see Diagnosis) - [ ]
scanner_worker_jobs_failed_total{reason=...}increasing for the affected job/queue (this is the failure counter the worker actually emits) - [ ] Doctor reports
check.scanner.resourcesat Fail (“Scanner resources critical: Memory …”) when the scanner exposes resource stats
NOT IMPLEMENTED: the original draft referenced a
ScannerWorkerOOMalert and ascanner_worker_restarts_total{reason="oom"}metric. Neither exists in source. The worker meter (ScannerWorkerMetrics) emitsscanner_worker_jobs_failed_total,scanner_worker_jobs_completed_total,scanner_worker_job_duration_ms,scanner_worker_stage_duration_ms, andscanner_worker_queue_latency_ms(plus cache/secret/surface counters). If you wire an alert, derive it from these counters or from container restart/OOM events at the orchestration layer.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Large images cannot be scanned; smaller images may still work |
| Data integrity | No data loss; failed scans can be retried (the lease is released and re-leased up to Scanner:Worker:Queue:MaxAttempts, default 5) |
| SLA impact | Specific images blocked from release pipeline |
Diagnosis
Quick checks
Run the Doctor resource check. This is the canonical health probe; it reads memory/CPU/worker-pool stats and fails at ≥95% memory (warns at ≥80%):
stella doctor --check check.scanner.resourcesCaveat: the check calls
GET {Scanner:Url}/api/v1/resources/stats. That endpoint is not currently implemented inStellaOps.Scanner.WebService, so on a stock deployment the check degrades to a Warn (“Cannot retrieve resource stats: HTTP 404 / Cannot check scanner resources”). Treat a Warn here as “stats unavailable”, and fall back to container-level memory inspection (docker stats/ cgroup memory on the host running the worker).Inspect the failing image’s size and layer count via the OCI inspector. Large total uncompressed size or a very high layer count is the usual trigger:
stella image inspect <image-ref> --print-layers --output jsonThe inspect output enumerates layers (size, digest, media type). There is no separate
stella image layers/--sort-bycommand —--print-layers(default on) oninspectis the layer view.Check the worker’s memory-bounding configuration. The worker reads the
Scanner:Workerconfiguration section. The two knobs that bound memory are:Scanner:Worker:Pipeline:MaxExtractedBytes— hard cap on total uncompressed bytes extracted for one image (decompression-bomb guard). Default 8 GiB (8L * 1024 * 1024 * 1024). Enforced byLayerArchiveExtractorinBuildFilesystemStageExecutor.Scanner:Worker:MaxConcurrentJobs— number of jobs processed in parallel per worker process. Default 2. Each in-flight job materializes a rootfs, so peak memory scales with this value.
These are configured via environment/appsettings overlays (e.g.
Scanner__Worker__MaxConcurrentJobs=1), not via astella scanner configCLI verb (that verb is NOT IMPLEMENTED; the workerappsettings/ container env is the source of truth).
Deep diagnosis
Reproduce the scan with verbose logging and read the worker logs. The worker logs the stage in flight via
ScannerWorkerInstrumentation; correlate the OOM/exit-137 with the last stage to start. Memory blow-ups are almost always in thebuild-filesystemstage (layer extraction) or a language/native analyzer stage processing a directory with very many files (e.g.node_modules). Read the worker container logs directly (e.g.docker logs <worker>/ orchestrator log aggregation) — there is nostella scanner logscommand.Confirm whether the extraction guard tripped. A clean abort with the message
build-filesystem: extraction exceeded the <N>-byte limit.(anInvalidOperationExceptionfromLayerArchiveExtractor) means the image exceedsScanner:Worker:Pipeline:MaxExtractedBytes; this is the guard working as designed (the worker fails the job rather than exhausting the host). A rawOutOfMemoryExceptionor exit 137 with no guard message means the host/cgroup limit was hit before the 8 GiB extraction cap — lowerMaxConcurrentJobsor raise the container memory limit.Check the secrets stage limits if the secrets scan is enabled. When
Scanner:Worker:Secrets:Enabled=true, per-file and per-job caps apply:Secrets:MaxFileSizeBytes(default 5 MB) andSecrets:MaxFilesPerJob(default 10 000). Images with huge text trees can pressure memory here.
Resolution
Immediate mitigation
Reduce per-worker concurrency so peak memory drops (each job holds its own rootfs). Set in the worker’s environment/appsettings and restart the worker container:
# appsettings / container env overlay (NOT a CLI verb): Scanner__Worker__MaxConcurrentJobs=1 # then restart the scanner-worker container via your orchestratorRaise the worker container memory limit at the orchestration layer (compose
mem_limit/ Kubernetes resources / systemdMemoryMax). Stella does not own a process-internal “memory limit” setting — memory is bounded by the container limit plus the extraction/concurrency caps above. Recommend ≥4 GiB for general use; size for the largest expected image.Re-submit the failed scan. A failed lease is retried automatically up to
Scanner:Worker:Queue:MaxAttempts(default 5). To re-run manually, re-issue the scan through your normal submission path (the worker pulls jobs from the queue; there is nostella scan image --retryflag).
Root cause fix
For consistently oversized images:
Raise the extraction cap only if the host can back it; otherwise keep the guard and reject the image. The cap exists to protect the host from decompression bombs:
# appsettings / container env overlay — value in bytes (example: 12 GiB) Scanner__Worker__Pipeline__MaxExtractedBytes=12884901888Pair any increase with a matching bump to the container memory limit.
Run a dedicated worker pool/replica set sized for large images. The CLI tracks a worker pool name today (
stella scanner workers set --count <n> --pool large-imageswrites a local worker-config file consumed bystella scan run); actual horizontal scaling and routing of large images to that pool is an orchestration-layer concern (replica count, memory request, queue routing), not an in-product router. The Doctor check’s remediation steps (stella scanner scale --memory +2G --cpu +2,stella scanner config set MaxConcurrentJobs 2, andstella scanner workers scale --replicas 4) are surfaced as Manual actions for this reason and are NOT IMPLEMENTED as CLI verbs (the only realscannersubcommands aredownload,workers get/set,secrets, andplugins) — perform the scaling through your orchestrator and setMaxConcurrentJobsvia the worker env/appsettings overlay.
For images with many small files (node_modules, etc.):
- Lower
Scanner:Worker:MaxConcurrentJobs(fewer simultaneous rootfs trees in memory) and/or constrain the secrets stage file budget (Scanner:Worker:Secrets:MaxFilesPerJob). There is nosbom.incremental_modesetting (NOT IMPLEMENTED — removed from the original draft).
Roadmap / Draft: streaming SBOM generation, incremental SBOM mode, and automatic layer-dedup caching keyed by base-image digest were described in the original draft but are not present in
src/Scannertoday. Track them as future work rather than operational levers; do not configure the (non-existent)sbom.streaming_threshold,sbom.incremental_mode, orcache.layer_dedupkeys.
Verification
# Re-run Doctor's resource check (canonical health gate)
stella doctor --check check.scanner.resources
# Re-inspect the previously failing image to confirm size/layers
stella image inspect <image-ref> --print-layers --output json
# Re-submit the scan through your normal path and confirm it completes
# (watch scanner_worker_jobs_completed_total vs scanner_worker_jobs_failed_total
# in your metrics backend, and tail the worker container logs for the run)
Prevention
- [ ] Capacity: Size the worker container memory limit for the largest expected image (recommend ≥4 GiB) and keep
MaxConcurrentJobsconsistent with that limit (peak ≈ per-job footprint ×MaxConcurrentJobs). - [ ] Guard rails: Leave
Scanner:Worker:Pipeline:MaxExtractedBytesenabled (default 8 GiB) so decompression bombs fail the job instead of the host. - [ ] Monitoring: Alert on rising
scanner_worker_jobs_failed_total{reason=…}and on container OOM-kill / restart events at the orchestration layer (there is no built-inscanner_worker_memory_usage_bytesgauge — thecheck.scanner.resourcesDoctor check is the in-product memory signal, and it depends on the/api/v1/resources/statsendpoint being available). - [ ] Documentation: Document image size limits in the user guide.
Related Resources
- Architecture:
docs/modules/scanner/architecture.md - Doctor check article:
docs/doctor/articles/scanner/resources.md(the runbook URL theScannerResourceUtilizationCheckitself links to) - Related runbooks:
scanner-worker-stuck.md,scanner-timeout.md,scanner-sbom-generation-failed.md - Source of truth:
- Worker options:
src/Scanner/StellaOps.Scanner.Worker/Options/ScannerWorkerOptions.cs - Worker metrics:
src/Scanner/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerMetrics.cs - Doctor check:
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Scanner/Checks/ScannerResourceUtilizationCheck.cs
- Worker options:
- Auth scopes (scanner):
scanner:read,scanner:scan,scanner:write,scanner:export— seesrc/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs - Dashboard: Grafana > Stella Ops > Scanner (worker job/stage duration + failure counters)
