Container Routing: Why the Same Agent Lands in Different Containers
This article details a four-step container routing mechanism (authentication, permission, routing, resolution) for an Agent platform, explaining how requests reach different container forms based on identity and instance health, and why distinguishing hard vs soft failures is critical for debugging.
01 | Global View: Four "Sent to Wrong Container" Incidents
The author uses a hospital triage analogy to explain container routing:
Registration (who are you) → Authentication: confirm identity
Triage (which department) → Permission: which container form allowed
Guide slip (building, floor, room) → Routing: pick form and instance
Room number → Resolution: address and port
Expert clinic (specific doctor) → Development form: target the active workspace
General clinic (any available) → Pooled usage form: bind a pre-warmed instance
Consultation center → Evolution form: run evolution tasks
Emergency channel → Shared/visitor: bypass normal triage
Registered but doctor absent → No available container: auth passed, pool empty
Fake registration → Auth failure: retry useless
Four real incidents illustrate the gap between symptoms and mechanism:
Scene A: IDE shows wrong files — Opened IDE, sees someone else's files. Common misdiagnosis: cache/browser issue. Mechanism truth: request landed in pooled container lacking user's workspace.
Scene B: Site works, chat all red — Page loads, chat returns 502. Common misdiagnosis: gateway down. Mechanism truth: front gateway alive, pre-gateway resolved no live instance.
Scene C: Refresh changes container — Behavior changes after refresh. Common misdiagnosis: load balancer jitter. Mechanism truth: form priority shifted: dev container recycled, fell back to pooled.
Scene D: No permission vs no resource look same — Both return same 5xx. Common misdiagnosis: unified error code for frontend simplicity. Mechanism truth: auth failure (hard) vs no capacity (soft) require different handling.
Routing's first gate is not a forwarding rule, but three judgments plus one landing: who you are, which form you may enter, which instance in that form is alive, and what is its address.
02 | Why "Just Reverse Proxy" Is Not Enough
Treating the pre-gateway as a simple forwarder breaks in three places:
2.1 First Fracture: Auth and Routing Conflated
Auth answers "who", routing answers "where". Merging them collapses failure codes: unauthorized, form not permitted, pool empty all return same 5xx. Frontend cannot distinguish, users refresh blindly, potentially triggering form fallback (reproducing Scene C).
2.2 Second Fracture: Assuming One Agent = One Container Form
Same Agent serves different purposes: developers need writable workspace, evolution runs batch jobs, users just chat, visitors only view. Four forms have different mounts, sidecars, and recycling policies. Forcing one form gives developers read-only containers or burdens users with heavy IDE-equipped containers.
2.3 Third Fracture: Resolving to Service Name Is Not Enough
A service name maps to a set of instances. Picking a form only selects a service; must still know which instances passed health checks, which are being recycled, which are still warming up. "In service list" ≠ "can serve requests". Skipping the liveness filter causes periodic Scene B recurrences.
Comparison of Approaches
Forward by path prefix — Gain: quick config. Reject: form judgment, explainable failures.
One Agent fixed to one container — Gain: simple mental model. Reject: density, recycling, multi-purpose coexistence.
Resolve to service name and return — Gain: one less query. Reject: recycling/warming instances get hit.
Four-step judgment + form decision tree + liveness check — Gain: failures separable, forms explainable. Reject: maintain a form priority table.
03 | Overall Design: Four Steps and Four Forms
Routing is not forwarding; it is three judgments plus one landing.
Auth — Answers: Who are you. Key design: User / integrated app / internal call — three distinct verification paths.
Permission — Answers: Which forms allowed. Key design: outputs an ordered list of allowed forms per Agent and identity; order = priority.
Routing — Answers: Which instance in that form. Key design: form priority × location × instance liveness.
Resolution — Answers: What is the address. Key design: assemble IP:port for reverse proxy; failure must indicate which step blocked.
Two Independent Dimensions
Dimension 1: Form (which container) — Values: Develop / Evolution / Pooled / Shared. Decides: which service, which instance group. Change frequency: varies with role and purpose. Failure mode: no available container (soft fail).
Dimension 2: Location (which port) — Values: Chat / Web IDE / Workspace Sync / File Proxy / Admin. Decides: which port, which identity headers. Change frequency: varies with entry type. Failure mode: 404 or protocol error (routing table collapse).
Form answers "which machine", location answers "which door". Flattening both into a single routing table is the gateway's earliest debt.
Four-Form Decision Tree
Develop — When selected: has workspace write permission, needs to edit. Sidecars: runtime + Web IDE + sync sidecar. Recycling: whitelisted, can stay alive long.
Evolution — When selected: evolution task running. Sidecars: runtime + batch components. Recycling: recycle when task ends.
Pooled — When selected: regular user chatting. Sidecars: chat runtime mainly. Recycling: idle recycle with warning.
Shared — When selected: shared access without dedicated permission. Sidecars: minimal. Recycling: public pool, scales with water level.
Priority order: If develop form possible, use it — it targets the workspace being edited. Like hospital: expert clinic first, general clinic only if expert unavailable; not because general is worse, but expert aligns with your records.
04 | Auth & Permission: Who Is Knocking
Three Identities, Three Verification Paths
User — Portal/IM chat users. Verification: platform session. Failure nature: hard fail, retry useless.
Integrated App — OpenAPI/MCP consumers. Verification: app ID + signature + time window. Failure nature: hard fail, but rotatable per app.
Internal Call — Orchestration, sync sidecar. Verification: internal credential + source check. Failure nature: hard fail, usually misconfiguration.
Permission Outputs an Ordered Form List
Permission step must not return a boolean but an ordered list of allowed forms . Router picks first form with live instances. Changing priority only reorders list; routing code unchanged.
// TypeScript
// Guarantee: auth failure and no capacity must yield distinct failure reasons
type ContainerForm = "develop" | "evolution" | "pool" | "shared";
type RouteResult =
| { ok: true; form: ContainerForm }
| { ok: false; kind: "unauthorized" | "no_capacity" };
function classifyRoute(input: {
identityOk: boolean;
allowedForms: ContainerForm[];
liveCount: Record<ContainerForm, number>;
}): RouteResult {
if (!input.identityOk || input.allowedForms.length === 0) {
return { ok: false, kind: "unauthorized" };
}
const picked = input.allowedForms.find(f => input.liveCount[f] > 0);
if (!picked) return { ok: false, kind: "no_capacity" };
return { ok: true, form: picked };
} # Python
# Same guarantee; list order = priority, fallback to shared only if none matched
def classify_route(identity_ok, allowed_forms, live_count):
if not identity_ok or not allowed_forms:
return {"ok": False, "kind": "unauthorized"}
for form in allowed_forms:
if live_count.get(form, 0) > 0:
return {"ok": True, "form": form}
return {"ok": False, "kind": "no_capacity"}Hard vs Soft Failures Must Be Separate
Hard Fail (unauthorized) — Meaning: identity or permission denied. Retry: useless. Frontend action: guide to request permission. Analogy: fake registration.
Soft Fail (no_capacity) — Meaning: permission granted, but no live instance. Retry: meaningful — wait for pool to replenish. Frontend action: show "Preparing environment" and auto-retry. Analogy: real registration, doctor not on duty.
These two failures must be distinguished at the registration window. Merging into "system busy" leaves user clueless whether to apply for access or just wait.
🛡 Boundary : Gateway only decides "can enter" and "which form". It does not create containers. Pool empty is orchestration's job; soft fail returned immediately — never block gateway threads waiting for a container.
05 | Routing & Resolution: Pick Form, Define Address
Form Decision: Take First Available in Order
// TypeScript
// Guarantee: form decision centralized, not scattered across call sites
function pickForm(ctx: {
canWriteWorkspace: boolean;
hasEvolutionTask: boolean;
isEndUser: boolean;
}): ContainerForm {
if (ctx.canWriteWorkspace) return "develop";
if (ctx.hasEvolutionTask) return "evolution";
if (ctx.isEndUser) return "pool";
return "shared";
} # Python
# Same guarantee; order consistent with TS to avoid drift
def pick_form(can_write_workspace, has_evolution_task, is_end_user):
if can_write_workspace:
return "develop"
if has_evolution_task:
return "evolution"
if is_end_user:
return "pool"
return "shared"Resolution: Service Name → Live Instance Filtering
Resolution performs three convergences: form + location → service → instance list → filter by liveness (health check, not recycling, warmed up). Skipping third step hits recycling or warming instances.
Convergence 1: Form + Location → Service — Input: form, entry type. Output: service name + default port. If skipped: IDE requests fed to chat port → random 404s.
Convergence 2: Service → Instance List — Input: service name. Output: set of instances. If skipped: requests hit already-decommissioned instances.
Convergence 3: Instances → Live Instances — Input: health checks, recycle state. Output: directly connectable address. If skipped: intermittent connection failures to recycling instances.
// TypeScript
// Guarantee: resolution returns null on miss; caller decides retry or error
function resolveTarget(form: ContainerForm, loc: Location): string | null {
const svc = serviceOf(form, loc);
const live = liveInstances(svc);
return live.length > 0 ? `${live[0].ip}:${live[0].port}` : null;
} # Python
# Same guarantee; miss returns None, caller handles soft fail branch
def resolve_target(form, loc):
svc = service_of(form, loc)
live = live_instances(svc)
return f"{live[0]['ip']}:{live[0]['port']}" if live else NoneFailure Must Indicate Which Step Blocked
Any of the four steps failing must log explicit step marker. Most time-saving debug log: "Stuck at resolution, form is pooled" — narrows scope from entire chain to one service.
Routing logs must answer "which step blocked", not just "forward failed".
06 | Two End-to-End Validation Paths
Scenario A: Developer Opens Web IDE
Path: Developer from portal → pre-gateway verifies session → permission returns ordered list [develop, pool] → form decision hits develop → by IDE location pick service/port → filter live instances → reverse proxy.
Decision point:
A : resolve agent directly to chat service, IDE reuses chat port.
B : form + location together decide port, IDE uses its own location.
Chose B. Reusing port saves a table but masks IDE failures as chat failures.
Scenario B: User Chats, Pool Empty
Path: user chats → gateway verifies session → permission returns [pool] → form hits pooled → resolution finds zero live instances → returns soft fail → frontend shows "Preparing environment" and retries → orchestration adds instance → retry resolves successfully.
Decision point:
A : gateway synchronously waits for instance to come up.
B : gateway immediately returns soft fail; "wait" moved to frontend + orchestration.
Chose B. Gateway thread pool not for waiting containers. Hospital analogy: triage desk doesn't block queue waiting for doctor; gives a "please wait" token and lets patient watch screen.
End-to-End Checklist
Failure separable — hard/soft failures use distinct codes; frontend handles separately.
Form explainable — any request can explain "why this form".
Resolution checks liveness — filters recycling and warming instances before proxy.
Location not collapsed — five entry types each have own port; failures reveal wrong door.
Log pinpoints step — failure log identifies which of four steps blocked.
Form fallback predictable — dev container recycled → behavior change expected and explainable.
07 | What This Design Delivers
⚡ Quantified efficiency — after killing misrouted containers and port collapse, tickets like "IDE shows others' files" and "intermittent 502" converged significantly (mechanism comparison).
📥 Capability pushdown — frontline can directly ask "which form did this request hit" without memorizing internal service names.
📚 Pattern upgrade — from "configure one forward rule" to "auth × permission × form × resolution" four verifiable steps.
Five-Dimension Selection
Failure separable — path prefix: poor; one agent one container: medium; resolve to service only: medium; four-step + form tree: good.
Multi-form coexistence — path prefix: poor; one agent one container: poor; resolve to service only: medium; four-step + form tree: good.
Debuggable — path prefix: poor; one agent one container: medium; resolve to service only: medium; four-step + form tree: high.
Implementation cost — path prefix: low; one agent one container: low; resolve to service only: medium; four-step + form tree: medium.
Recommendation — path prefix: demo only; one agent one container: single-purpose; resolve to service only: long-lived static instances; four-step + form tree: default .
08 | When Not to Copy This, and Four Pitfalls
Applicable / Not Applicable
Applicable : platform serving developers, evolution tasks, and end users for same agent; container forms change dynamically with recycling and warming.
Not applicable : internal tool with one agent, one purpose, one container; small deployment with static containers, no instance rotation.
Pitfall 1: Auth Failure and No Capacity Share One Error Code
Cause : both look like "request didn't get in". Wrong fix : unify to 5xx, frontend shows "system busy". Right fix : split into hard/soft fail; frontend guides differently — one to request permission, one to wait.
Pitfall 2: Form Priority Scattered Across Call Sites
Cause : each entry point independently decides "which container". Wrong fix : extract common function but call signatures differ. Right fix : permission stage emits ordered form list; routing simply "pick first with live instance".
Pitfall 3: Resolve to Service Name and Return
Initially instances long-lived, all fine. Once recycling and warming introduced, intermittent connections to recycling instances appear. Discovered after container recycling launch — add one liveness filter solves it; hard part is realizing "in service list" ≠ "can serve".
Pitfall 4: Gateway Synchronously Waits for Container
Wanted "seamless" for users, so gateway waited synchronously on resolution failure. Works off-peak; under load all requests pile in gateway thread pool, one hiccup slows entire site. Correct: soft fail + frontend retry + orchestration replenish — move "wait" off critical path.
L1 / L2 / L3 Change Governance
L1 — action: adjust form priority order, add failure codes. approver: platform team.
L2 — action: add location / adjust liveness criteria in resolution. approver: cross-gateway & orchestration review.
L3 — action: add new container form / change auth-to-routing semantics. approver: change gate + capacity assessment.
Summary
Routing is four judgments — auth, permission, routing, resolution — each can fail independently; never a single forward rule.
Hard and soft failures must be separate — one guides to request permission, one guides to wait; merging into "system busy" discards all judgment.
Same agent landing in different container forms is by design: developers need write, users need density, evolution needs batch.
Form answers "which machine", location answers "which door"; flattening into one routing table is the earliest debt.
Resolution must filter live instances: in service list ≠ can serve requests.
Failure logs must tell which step blocked; otherwise debugging restarts from whole chain.
The gateway is not a forward rule; it is a triage desk: first confirm who you are, then decide which clinic you may enter, finally hand you a slip with the room number.
Next article: Workspace Files & Pre-Gateway: Right Container, Right Disk — after entering the right container, where does file truth actually land.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
James' Growth Diary
I am James, focusing on AI Agent learning and growth. I continuously update two series: “AI Agent Mastery Path,” which systematically outlines core theories and practices of agents, and “Claude Code Design Philosophy,” which deeply analyzes the design thinking behind top AI tools. Helping you build a solid foundation in the AI era.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
