Difficulty: Beginner · You'll need: TypeScript, Go · Size: ~5 lines
What's going on
The platform gives up waiting 15 seconds before the agent stops trying.
| Layer |
Timeout |
Location |
Server, waiting for container.result |
45s |
agents.gateway.ts → sendContainerCommand(… timeoutMs = 45_000) |
Agent, docker restart |
60s |
docker/service.go → RestartContainer |
Agent, docker remove |
60s |
docker/service.go → RemoveContainer |
What the operator sees
A container that takes 50 seconds to restart or force-remove:
- At 45s the server's
setTimeout fires, deletes the pendingCommands entry, and rejects the promise
mapAndThrow matches 'timed out' → 504 Gateway Timeout
- The dashboard shows "Agent timed out waiting for container.result (restart)"
- At 50s the agent finishes successfully and sends
container.result
- The gateway logs
No pending command for container.result requestId=… and drops it
The operation succeeded. The operator was told it failed, and the next list refresh contradicts the error they just saw. For remove this is worse than for restart: the natural reaction to a failed delete is to click delete again, which then returns "No such container".
Why it happens
The two numbers were chosen independently and nobody compared them. It is not specific to remove — restart has had it since lifecycle commands were added.
Fixing it
The invariant to establish: the waiter must outlive the worker. The server's timeout has to exceed the agent's Docker timeout plus round-trip time, for every action.
Options:
- Raise the server timeout past the slowest agent-side operation, with headroom — e.g. 75s against a 60s worst case. One line, no protocol change.
- Per-action timeouts on both sides, derived from one shared table. More correct — a
start need not hold a request for 75s — but more moving parts.
- Have the agent send a progress/ack message so the server can extend. Over-engineered for this.
Option 1 is the right size for v0.1.0. Whichever is chosen, write the relationship down as a comment next to both numbers so the next person changing one sees the other.
Done when
Difficulty: Beginner · You'll need: TypeScript, Go · Size: ~5 lines
What's going on
The platform gives up waiting 15 seconds before the agent stops trying.
container.resultagents.gateway.ts→sendContainerCommand(… timeoutMs = 45_000)docker restartdocker/service.go→RestartContainerdocker removedocker/service.go→RemoveContainerWhat the operator sees
A container that takes 50 seconds to restart or force-remove:
setTimeoutfires, deletes thependingCommandsentry, and rejects the promisemapAndThrowmatches'timed out'→ 504 Gateway Timeoutcontainer.resultNo pending command for container.result requestId=…and drops itThe operation succeeded. The operator was told it failed, and the next list refresh contradicts the error they just saw. For
removethis is worse than forrestart: the natural reaction to a failed delete is to click delete again, which then returns "No such container".Why it happens
The two numbers were chosen independently and nobody compared them. It is not specific to
remove—restarthas had it since lifecycle commands were added.Fixing it
The invariant to establish: the waiter must outlive the worker. The server's timeout has to exceed the agent's Docker timeout plus round-trip time, for every action.
Options:
startneed not hold a request for 75s — but more moving parts.Option 1 is the right size for v0.1.0. Whichever is chosen, write the relationship down as a comment next to both numbers so the next person changing one sees the other.
Done when