{"record":{"id":"3ece2cd93ea6ce56","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-start-sop-cooldown-or-concurrency-lim","errorCode":null,"errorMessage":"Cannot start SOP '{}': cooldown or concurrency limit reached","messagePattern":"Cannot start SOP '(.+?)': cooldown or concurrency limit reached","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/engine.rs","lineNumber":929,"sourceCode":"    pub fn is_gate_reference_superseded(&self, run_id: &str, reference_revision: u32) -> bool {\n        self.active_runs.get(run_id).is_some_and(|run| {\n            run.revision != reference_revision && !self.is_park_persist_pending(run_id)\n        })\n    }\n\n    /// Admit a run through the store CAS claim before it becomes locally active.\n    /// The durable store is the concurrency source of truth; `active_runs` is the\n    /// execution cache/status surface.\n    fn claim_admission(&self, run_id: &str, sop: &Sop) -> Result<ClaimToken> {\n        match self.store.try_claim_run(\n            run_id,\n            &sop.name,\n            sop.max_concurrent as usize,\n            self.config.max_concurrent_total,\n        ) {\n            Ok(Some(token)) => Ok(token),\n            Ok(None) => {\n                bail!(\n                    \"Cannot start SOP '{}': cooldown or concurrency limit reached\",\n                    sop.name\n                );\n            }\n            Err(e) => Err(anyhow::Error::new(e)),\n        }\n    }\n\n    fn release_claim_best_effort(&self, token: &ClaimToken) {\n        if let Err(e) = self.store.release_claim(token) {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Unknown)\n                    .with_attrs(::serde_json::json!({\n                        \"run_id\": token.run_id.as_str(),\n                        \"error\": e.to_string(),\n                    })),","sourceCodeStart":911,"sourceCodeEnd":947,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/engine.rs#L911-L947","documentation":"claim_admission() (reached via reserve_run_slot) asks the admission ledger for a start token and got None: the SOP is in cooldown, its per-SOP max_concurrent is exhausted, or the global max_concurrent_total is reached. The SOP start is refused — this is backpressure, not corruption.","triggerScenarios":"Starting a SOP (sop_execute / start paths that reserve a run slot) while: the SOP's cooldown window after a prior run is active, exec counts for the SOP are at max_concurrent, or total running executions are at config.max_concurrent_total.","commonSituations":"Bursty triggers (webhooks, cron fan-out) exceeding configured concurrency; cooldown set to debounce repeated triggers and a legitimate replay arriving inside the window; max_concurrent_total tuned too low for the workload; stuck runs holding slots.","solutions":["Wait for in-flight runs of the SOP to finish (or its cooldown to elapse) and retry the start.","If the load is legitimate, raise the SOP's max_concurrent and/or the global max_concurrent_total in config.","Check for stuck/long-running runs holding slots — resolve or time them out.","For bursty sources, prefer the dispatch path with an admission policy (Coalesce/Defer) instead of direct starts."],"exampleFix":"# before\nmax_concurrent = 1\n# burst of 3 triggers -> 2nd and 3rd starts bail\n\n# after\nmax_concurrent = 3\n# (or route triggers through dispatch with coalesce/defer policy)","handlingStrategy":"retry","validationCode":"match engine.evaluate_admission(sop_name) {\n    SopAdmission::Admit => engine.reserve_run_slot(sop_name),\n    other => handle_non_admit(other), // wait, coalesce, or surface reason — do not call reserve\n}","typeGuard":null,"tryCatchPattern":"let mut delay = Duration::from_millis(500);\nloop {\n    match engine.start_sop(sop_name, input.clone()) {\n        Ok(run) => break run,\n        Err(e) if e.to_string().contains(\"cooldown or concurrency limit reached\") && delay <= Duration::from_secs(30) => {\n            tokio::time::sleep(delay).await;\n            delay *= 2;\n        }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Size max_concurrent and max_concurrent_total to your real trigger load; measure before tuning down.","Watch for stuck runs holding slots — alert on long-executing SOP runs.","Prefer the dispatch API with admission policies for bursty triggers instead of direct start calls."],"tags":["sop","admission","concurrency","cooldown","backpressure","rate-limit"],"backgroundTag":"rate-limit-reached","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}