{"record":{"id":"aa468c9e86814bb4","repo":"astrid-runtime/astrid","slug":"mcp-gateway-attach-limit-reached-for-principal-p","errorCode":null,"errorMessage":"MCP gateway attach limit reached for principal '{principal}' ({MAX_ATTACHES})","messagePattern":"MCP gateway attach limit reached for principal '(.+?)' \\((.+?)\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/mcp/gateway.rs","lineNumber":219,"sourceCode":"        self.permits\n            .lock()\n            .await\n            .entry(principal.to_owned())\n            .or_insert_with(|| Arc::new(Semaphore::new(MAX_ATTACHES)))\n            .clone()\n    }\n\n    async fn acquire(&self, principal: &str) -> Result<OwnedSemaphorePermit> {\n        let semaphore = self.semaphore_for(principal).await;\n        if let Ok(permit) = semaphore.clone().try_acquire_owned() {\n            return Ok(permit);\n        }\n        if self.evict_lru_idle().await\n            && let Ok(permit) = semaphore.try_acquire_owned()\n        {\n            return Ok(permit);\n        }\n        anyhow::bail!(\n            \"MCP gateway attach limit reached for principal '{principal}' ({MAX_ATTACHES})\"\n        )\n    }\n\n    /// Reserve a host session through replacement, cap admission, and slot\n    /// installation as one linearizable operation. The reservation owns the\n    /// admission guard until `install` publishes the new slot.\n    async fn reserve_session(\n        &self,\n        host_session_id: &str,\n        principal: &str,\n    ) -> Result<AttachReservation> {\n        let admission = Arc::clone(&self.admission).lock_owned().await;\n        if self.shutdown.is_cancelled() {\n            anyhow::bail!(\"MCP gateway is shutting down\");\n        }\n        self.replace_session(host_session_id).await?;\n        let permit = self.acquire(principal).await?;","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/mcp/gateway.rs#L201-L237","documentation":"The MCP gateway caps each principal at MAX_ATTACHES = 16 live attach sessions. In `acquire`, after trying a plain semaphore permit and then evicting the least-recently-used idle slot, if no permit can be obtained the gateway rejects the attach. This is an intentional process-local resource cap so a hostile host cannot make one principal consume an unbounded number of broker sessions.","triggerScenarios":"Calling reserve_session (via acquire) when the principal already holds 16 live attach slots and none of them are idle enough to be LRU-evicted; also hit when a replacement attach cannot be admitted within REPLACEMENT_TIMEOUT because the predecessor has not released its slot.","commonSituations":"Opening more than 16 concurrent MCP attach sessions for the same principal (e.g. many editor windows or agent processes each running `mcp attach`); leaked/stale sessions that are still considered active so LRU eviction cannot free a slot; reconnect storms during flaky network conditions.","solutions":["Reduce the number of concurrent `mcp attach` sessions for this principal to at most 16; close or disconnect idle/stale attach processes.","Check for orphaned gateway attach processes (ps / lsof on the Unix socket) and kill stale ones so their slots free up.","Retry the attach after existing sessions go idle or EOF; idle slots are evicted LRU automatically.","If 16 is genuinely too small for your workload, this cap is a compile-time constant (MAX_ATTACHES) and requires a code change, not configuration."],"exampleFix":"// before: many parallel attach clients each opening their own session\nfor i in 0..32 { spawn(mcp_attach(principal)); }\n// after: reuse one attach session per host-session id, or close idle ones\nfor i in 0..32 { spawn(mcp_attach(principal)).join().unwrap(); } // or pool <= 16 sessions","handlingStrategy":"validation","validationCode":"fn can_attach(live_sessions: usize, all_active: bool) -> bool {\n    live_sessions < 16 || !all_active // a slot must be free or LRU-evictable\n}","typeGuard":null,"tryCatchPattern":"match gateway.reserve_session(host_session_id, principal).await {\n    Err(e) if e.to_string().contains(\"attach limit reached\") => queue_attach_for_retry(),\n    other => other?,\n}","preventionTips":["Pool attach sessions; one per host-session id, capped well below 16.","Tear down attach sessions on editor/agent exit so slots are released.","Detect orphaned attach processes before starting new ones.","Retry with backoff on this error instead of spawning more sessions."],"tags":["mcp","resource-limit","gateway","concurrency"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}