Status: Accepted Date: 2026-10-02
Issue #742 (API-7) asks for predictable behavior under slow, concurrent,
malformed or overloaded clients: request timeout, header and body limits, a
concurrency cap, bounded queues, Retry-After on overload, graceful drain,
and a documented connection policy. Its multi-user note says a public or
network deployment must use a reviewed production transport adapter rather
than expose the stdlib development server.
Measured state of idkmesh/control_tower_ui.py before this decision:
ControlTowerServer is a stdlib ThreadingHTTPServer with
daemon_threads = True: one thread per connection and no cap.MAX_BODY_BYTES, 2 MiB), enforced on the
single endpoint that reads a body.serve_control_tower calls server_close() on exit but never waits for an
in-flight request, because daemon threads are not joined.Apply the following limits to the Control Tower development server, publish them, and prove each one with a test.
408 request_timeout where
the socket still allows a response.503 overloaded with a
Retry-After header and the standard error envelope, without doing any
application work. GET /healthz is exempt so liveness stays cheap.request_queue_size = 16). The cap bounds concurrent request handling, not
accepted connections; this is stated rather than implied.503 shutting_down (including /readyz, so an orchestrator stops routing)
while /healthz still answers. serve_control_tower begins a drain on
shutdown and waits up to 5 s for in-flight requests before closing. Every
endpoint is read-only or a pure inspection, so no drain can leave a
partially committed mutation.429 in v0.1. 429 Retry-After signals that one client exceeded its
own rate. With a single local token there is no per-client identity to
attribute a rate to, so emitting 429 would be fabricated precision.
Server-wide saturation is 503. 429 is reserved for the enterprise
identity profile (ADR-0014/0016) and recorded as not implemented.operations.limits object reporting the limits actually in force.
Adding an optional property is a non-breaking change under ADR-0020, so the
frozen status schema is extended in place rather than versioned.finally path even when the client disconnects before
the response is written. The server does not retry interrupted application
work. SSE disconnects terminate the stream on broken-pipe/reset/timeout and
release the separate stream slot. Because this local profile is read-only
plus pure inspection, an abandoned response cannot leave a partially
committed mutation.Tunables are exposed as idkmesh control-tower --request-timeout SECONDS and
--max-concurrent-requests N, validated to sane ranges.
503 plus Retry-After.503. The backlog bound and the
loopback-only bind keep this acceptable for a development server.503.429 remains unimplemented until a trustworthy per-client
identity exists. SSE capacity is now implemented under ADR-0023 with a
separate limiter so long-lived streams do not consume general request slots.gate-audit-ui, steward UIs); they stay as before.Rejected for this slice. It adds a dependency to a base install that #742 requires stay dependency-light, and the issue itself defers the network profile to a reviewed transport adapter.
429 for concurrency saturationRejected. It conflates server overload with a per-client quota and gives clients wrong retry semantics.
Rejected. An unbounded wait queue is an unbounded memory and latency commitment, which is exactly what #742 asks to remove.
Rejected. Rejecting before parsing cannot exempt /healthz or return the
standard envelope with the request id and security headers.
idkmesh/service_runtime.py owns the reusable limiter and limit constants;
idkmesh/control_tower_ui.py applies them;
docs/specifications/CONTROL_TOWER_LOCAL_API_V0_1.md documents them.Revisit this ADR if:
429 and per-principal quotas) or a production transport adapter (move the
limits there);