The main (5678) and FIPS peer/federation (5679) listeners shared one
1024-permit connection semaphore, and accept_loop awaited that permit
before spawning the connection handler — inside the accept loop itself.
When federation peer connections piled up in CLOSE-WAIT without ever
completing, they exhausted the shared pool and froze accept() entirely,
taking the web UI down with them (production outage, 2026-07-24: no
login possible, auth.isSetup/server.echo hung indefinitely even hitting
the backend directly).
Two changes:
- Acquire the permit inside the spawned task (with a bounded 30s wait)
instead of in the accept loop, so a saturated pool can no longer
block accept() from continuing to accept new connections.
- Give the peer listener its own semaphore, separate from the main
listener's, so peer/federation congestion can never starve local
web UI connections again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>