Runbook: Policy Engine - Evaluation Latency High
For platform on-call. Use this when Stella Ops policy evaluation is slow enough to delay release gates or CI/CD pipelines — elevated policy_run_seconds / policy_evaluation_latency_seconds P95, timeouts, or saturated workers. It walks you through diagnosing the slow phase (compilation, evaluation, caching, reachability/exception lookups) and the real tuning levers. The primary evaluator is the native C# DSL engine; OPA/Rego is an interop adapter, not the hot path.
Sprint: SPRINT_20260117_029_DOCS_runbook_coverage Task: RUN-003 - Policy Engine Runbooks
Reconciliation note (doc<->code): This runbook was authored against an assumed OPA/Rego-as-the-evaluator model. The Policy Engine’s primary evaluator is the native C# DSL engine (
PolicyExpressionEvaluator/PolicyRuntimeEvaluationServiceoverStellaOps.PolicyDsl); OPA/Rego is an interop adapter for teams on OPA-based toolchains, not the core hot path (seedocs/modules/policy/architecture.md§13: “The C# engine remains primary; Rego serves as an interoperability adapter”). The diagnosis and resolution steps below have been corrected to match the shipped CLI surface, metric names, config keys, and Doctor check. Steps that referenced commands/config/metrics that do not exist in the codebase have been removed or flagged. Source of truth:src/Policy/**.
Metadata
| Field | Value |
|---|---|
| Component | Policy Engine |
| Severity | High |
| On-call scope | Platform team |
| Last updated | 2026-05-30 |
| Doctor check | check.policy.engine |
The Doctor check id is
check.policy.engine(source:src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Policy/Checks/PolicyEngineHealthCheck.cs). There is nocheck.policy.evaluation-latencycheck; the engine health check covers compilation, evaluation, and storage and emits a latency warning when probe evaluation exceeds 100 ms.
Symptoms
- [ ] Policy evaluation latency high —
policy_evaluation_latency_seconds(per-batch) orpolicy_run_seconds(per run) P95 elevated - [ ] Gate / promotion decisions timing out in CI/CD pipelines
- [ ]
policy_evaluation_failures_total{reason="timeout"}incrementing - [ ]
policy_worker_utilizationnear1.0orpolicy_run_queue_depthgrowing - [ ] Doctor reports
Policy engine health check passed with warningswith aperformance_warning(evaluation time exceeds 100 ms threshold) - [ ] Users report “policy check taking too long”
Metric names verified against
src/Policy/StellaOps.Policy.Engine/Telemetry/PolicyEngineTelemetry.cs. The previously-documented metricpolicy_evaluation_duration_secondsdoes not exist and the alert namePolicyEvaluationSlowis not defined in the repo — both removed.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Slow release gate checks, CI/CD pipeline delays |
| Data integrity | No data loss; decisions are still correct and deterministic |
| SLA impact | Gate latency SLO at risk (track via policy_slo_burn_rate / policy_error_budget_remaining) |
NOTE: No numeric latency SLO target (e.g. “P95 < 500ms”) is defined in source. The engine exposes SLO instruments (
policy_slo_burn_rate,policy_error_budget_remaining,policy_slo_violations_total) but the thresholds are supplied by deployment-side alerting config, not the code. Set your own target during alert tuning rather than treating a fixed number as a product guarantee.
Diagnosis
Quick checks
Run the Policy Engine Doctor check:
stella doctor --check check.policy.engineReports
compilation_status,evaluation_status,storage_status,policy_count,evaluation_latency_p50_ms, andcache_hit_ratio. Aperformance_warningis emitted when probe evaluation exceeds 100 ms.The check requires
Policy:Engine:Url(orPolicyEngine:BaseUrl) to be configured; it probes an OPA-compatible HTTP endpoint (/health,/v1/policies,/v1/data). If the engine is deployed without that URL, the check is skipped (CanRunreturns false).Inspect latency / saturation metrics (Prometheus/Grafana, meter
StellaOps.Policy.Engine):policy_run_seconds— per-run duration histogram (tags:mode,tenant,policy,outcome)policy_evaluation_latency_seconds— per-batch evaluation latency (tags:tenant,policy)policy_concurrent_evaluations/policy_worker_utilization— saturation gaugespolicy_run_queue_depth— pending-run backlog per tenantpolicy_evaluation_failures_total{reason="timeout"}— timed-out evaluations
Review structured logs / traces to find the slow phase. Evaluation is traced with spans
policy.select,policy.evaluate,policy.materialize, andpolicy.simulate(source:PolicyEngineTelemetry.Start*Activity). Logs carrytraceId,policyId,version,runId,tenant,phase.FLAGGED (removed): the prior steps
stella policy status,stella policy stats --last 10m,stella policy evaluate --profile,stella policy cache stats,stella policy analyze --complexity, andstella policy logs --filter ...do not exist in the CLI. The realstella policysubcommands are:simulate,activate,lint,edit,test,new,history,explain,init,compile,version,submit,review,publish,rollback,sign,verify-signature,lattice,verdicts,promote, plus pack/distribution commands (validate-yaml,install,list-packs,push,pull,export-bundle,import-bundle). Diagnosis therefore relies on metrics/logs/traces and the Doctor check rather than a dedicated stats/profile CLI.
Deep diagnosis
Reproduce against a known input with
simulate:stella policy simulate <policy-id> --candidate <version> --sbom <sbom-id> --explain--explainrequests explain traces for diffed findings, which surface the rules that fired. Use--mode batchto exercise all matching SBOMs and reproduce the slow path at scale.Check compilation cost if the slow phase is policy load/compile:
- Metrics:
policy_compilation_seconds,policy_compilation_total{outcome}. - Compilation enforces guardrails from
PolicyEngine.Compilation:MaxComplexityScore(default750) andMaxDurationMilliseconds(default1500). Compilations exceeding these are rejected, not merely slowed — a borderline-complex pack can spike compile time.
- Metrics:
Check the evaluation result cache (deterministic Redis + in-memory fallback; source:
src/Policy/StellaOps.Policy.Engine/Caching/):- The Doctor check surfaces
cache_hit_ratio. A low hit ratio means most evaluations recompute from scratch. - Cache key is
pe:{policyDigest}:{subjectDigest}:{contextDigest}— changes to policy, subject, or context digests legitimately miss the cache.
- The Doctor check surfaces
Check reachability / exception lookups (these run inline during evaluation and can dominate latency):
policy_reachability_lookup_seconds,policy_reachability_cache_hit_ratio,policy_reachability_cache_hits_total/_misses_total.policy_exception_load_latency_seconds,policy_exception_application_latency_seconds,policy_exception_loaded_total.
FLAGGED (removed): “external data fetches” diagnosis. The evaluator is forbidden from calling external HTTP except allow-listed internal services (Authority, Storage) — see architecture §7 “Side effects prohibited”. There is no general external-data fetch path to inspect; the relevant inline I/O is reachability-facts and exception loading (above).
Resolution
Immediate mitigation
Raise evaluation concurrency (the real worker knob):
// PolicyEngine config section (PolicyEngineOptions) "PolicyEngine": { "Workers": { "MaxConcurrentEvaluations": 8 // default 4 } }Source:
src/Policy/StellaOps.Policy.Engine/Options/PolicyEngineOptions.cs(PolicyEngineWorkerOptions). Restart the Policy Engine service to apply (there is no livestella policy reload/config setcommand — config is loaded at startup).FLAGGED (removed):
stella policy config set opa.workers 4andstella policy reload. Noopa.workersconfig key, nopolicy configcommand, and nopolicy reloadcommand exist.Tune the evaluation result cache. Caching is governed by
PolicyEngine.EvaluationCache(PolicyEvaluationCacheOptions) and backed by Redis with an in-memory fallback. Ensure Redis is reachable and warm so the per-batch cache (pe:{policyDigest}:{subjectDigest}:{contextDigest}) is effective; verify the resultingcache_hit_ratiovia the Doctor check.FLAGGED (removed):
stella policy cache clear/cache warmandstella policy config set cache.evaluation_ttl 60s. Nopolicy cachecommand and nocache.evaluation_ttlkey exist; cache behaviour is configured throughPolicyEngine.EvaluationCacheand the Redis backend.If the bottleneck is reachability or exception loading, ensure the reachability-facts cache and exception cache are configured and warm (
PolicyEngine.ReachabilityCache,PolicyEngine.ExceptionCache), and that their backing stores are healthy. Watchpolicy_reachability_cache_hit_ratioandpolicy_exception_load_latency_seconds.
Root cause fix
If a policy is too complex / compilation is slow:
Reproduce locally by compiling the DSL file:
stella policy compile <file> --optimize --format json--optimizeenables IR optimization passes; the output reports the SHA-256 digest of the compiled IR (suppress with--no-digest). Source:CommandFactory.BuildPolicyCommand/HandlePolicyCompileAsync.FLAGGED (removed):
stella policy compile --all. Thecompilecommand takes a singlefileargument; there is no--allflag. (The Doctor check’s remediation text still suggestsstella policy compile --all, which is itself inaccurate — track via the Doctor plugin.)Reduce complexity below the configured guardrail. The engine rejects packs over
PolicyEngine.Compilation.MaxComplexityScore(default750) orMaxDurationMilliseconds(default1500); split or simplify the policy so it compiles comfortably under those limits. Usestella policy lint <file>to surface structural issues.FLAGGED (removed):
stella policy analyze --suggest-optimizationsandstella policy refactor --auto-split. Neitheranalyzenorrefactorexists as astella policysubcommand.
If evaluations are timing out under load:
Increase
PolicyEngine.Workers.MaxConcurrentEvaluationsand/or scale out Policy Engine replicas, then re-checkpolicy_worker_utilizationandpolicy_run_queue_depth.Confirm the result cache is healthy (above) so repeat evaluations of unchanged subjects are served from cache rather than recomputed.
DRAFT / roadmap (not implemented): “Enable OPA partial evaluation” and “pre-compile policies” were carried over from the OPA-evaluator framing. The shipped primary engine is the native C# DSL evaluator, which has no OPA
partial_evaltoggle (opa.partial_evalis not a config key in the repo). TheEmbeddedOpaEvaluator(StellaOps.Policy.Interop) shells out to a bundledopabinary only to evaluate external Rego artifacts for interop; it is not on the gate hot path. If/when OPA-side performance tuning becomes a supported lever, document the concrete config here.
Verification
# Re-run the engine health check (latency + cache surfaced in evidence)
stella doctor --check check.policy.engine
# Reproduce the evaluation and confirm it completes within budget
stella policy simulate <policy-id> --candidate <version> --sbom <sbom-id> --explain
Then confirm in metrics that policy_run_seconds / policy_evaluation_latency_seconds P95 has recovered, policy_evaluation_failures_total{reason="timeout"} has stopped incrementing, and cache_hit_ratio is back to expected levels.
Prevention
- [ ] Review: Lint and compile policies (
stella policy lint,stella policy compile) before activation; keep complexity well underCompilation.MaxComplexityScore. - [ ] Monitoring: Alert on
policy_run_seconds/policy_evaluation_latency_secondsP95 and onpolicy_evaluation_failures_total{reason="timeout"}; trackpolicy_slo_burn_rate. - [ ] Caching: Keep the evaluation result cache (Redis) and reachability/exception caches healthy; monitor
cache_hit_ratioandpolicy_reachability_cache_hit_ratio. - [ ] Capacity: Watch
policy_worker_utilizationandpolicy_run_queue_depth; raiseWorkers.MaxConcurrentEvaluationsor add replicas before saturation.
Related Resources
- Architecture:
docs/modules/policy/architecture.md(see §7 Determinism/side-effects and §13 Interop Layer for the C#-primary / Rego-interop split) - Telemetry source of truth:
src/Policy/StellaOps.Policy.Engine/Telemetry/PolicyEngineTelemetry.cs - Config source of truth:
src/Policy/StellaOps.Policy.Engine/Options/PolicyEngineOptions.cs - Related runbooks:
policy-incident.md,policy-opa-crash.md,policy-compilation-failed.md,policy-storage-unavailable.md,policy-version-mismatch.md - Dashboard: Grafana > Stella Ops > Policy Engine
