Summary
The WebSocket feed intends to bound each slow client's queue (_WEBSOCKET_QUEUE_MAXSIZE = 32), but the supported application override app["callstack_ws_queue_size"] is converted with int(...) without range validation. Setting it to 0 creates asyncio.Queue(maxsize=0), which asyncio defines as unbounded. A stalled WebSocket client can then accumulate an unbounded in-process envelope backlog.
Root cause
In server.py:304-305, the endpoint does:
queue_size = int(request.app.get("callstack_ws_queue_size", _WEBSOCKET_QUEUE_MAXSIZE))
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=queue_size)
asyncio.Queue treats maxsize <= 0 as an infinite queue. _enqueue_websocket_envelope() only drops the oldest item after queue.full() is true, which never occurs for that queue configuration. The current overflow test covers a directly constructed bounded queue but not the endpoint configuration boundary.
Reproduction
Baseline main is healthy:
$ git diff --check
# exit 0
$ PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
1093 passed in 7.67s
No hardware or network service is required. A no-hardware aiohttp.test_utils probe replaces server.asyncio.Queue with a recording subclass, creates the app, sets app["callstack_ws_queue_size"] = 0, then opens /ws and reads its hello envelope. Observed output:
configured_queue_size=0
queue_maxsize_seen=[0]
queue_is_unbounded=True
Therefore the endpoint instantiates an unbounded queue, bypassing its explicit slow-client memory guard.
Impact
An operator or integration that configures a zero/negative queue size to mean "disabled" or makes a type-conversion mistake silently removes the per-client memory bound. Under a high event rate and a slow/disconnected client, this can cause memory growth in the HTTP process. Serialized envelopes are PII-safe, but unbounded process memory is still an availability risk for unattended Raspberry Pi deployments.
This is distinct from closed #228, which bounds EventBus stream queues, and from #213, which proposes bounded replay cursors. It is the runtime configuration boundary of the existing live WebSocket queue.
Suggested fix direction
Validate the queue size before the WebSocket upgrade/subscription is established:
- accept only a non-boolean positive integer (or explicitly document a bounded supported range);
- reject
0, negatives, booleans, non-numeric strings, and non-finite numeric inputs with a deterministic configuration error or safe HTTP 500/400 policy;
- keep the default value and the current drop-oldest, PII-safe overflow notice behavior unchanged.
Prefer a small helper so the server-level configuration contract is directly unit-testable.
Acceptance criteria
app["callstack_ws_queue_size"] = 0 and negative values cannot create an unbounded queue.
- Boolean, non-integer, and malformed override values fail predictably without creating a WebSocket subscription.
- The normal default still creates a queue with maxsize 32 (or its documented replacement).
- A valid positive override creates a bounded queue and retains drop-oldest/overflow behavior.
- Tests cover the endpoint/configuration boundary, not only
_enqueue_websocket_envelope() in isolation.
- Existing WebSocket auth, event filtering, serialization/redaction, and multi-client behavior remain green.
Verification gates
git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_websocket.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
Duplicate check
Searched open/closed issues and PRs for callstack_ws_queue_size, WebSocket queue maxsize zero, WebSocket slow client queue bound, and queue size configuration PII websocket. The only adjacent work found was closed #228 (EventBus stream bounds) and open #213 (replay cursors); neither tracks an invalid live WebSocket queue override creating asyncio.Queue(maxsize=0).
Summary
The WebSocket feed intends to bound each slow client's queue (
_WEBSOCKET_QUEUE_MAXSIZE = 32), but the supported application overrideapp["callstack_ws_queue_size"]is converted withint(...)without range validation. Setting it to0createsasyncio.Queue(maxsize=0), which asyncio defines as unbounded. A stalled WebSocket client can then accumulate an unbounded in-process envelope backlog.Root cause
In
server.py:304-305, the endpoint does:asyncio.Queuetreatsmaxsize <= 0as an infinite queue._enqueue_websocket_envelope()only drops the oldest item afterqueue.full()is true, which never occurs for that queue configuration. The current overflow test covers a directly constructed bounded queue but not the endpoint configuration boundary.Reproduction
Baseline
mainis healthy:No hardware or network service is required. A no-hardware
aiohttp.test_utilsprobe replacesserver.asyncio.Queuewith a recording subclass, creates the app, setsapp["callstack_ws_queue_size"] = 0, then opens/wsand reads itshelloenvelope. Observed output:Therefore the endpoint instantiates an unbounded queue, bypassing its explicit slow-client memory guard.
Impact
An operator or integration that configures a zero/negative queue size to mean "disabled" or makes a type-conversion mistake silently removes the per-client memory bound. Under a high event rate and a slow/disconnected client, this can cause memory growth in the HTTP process. Serialized envelopes are PII-safe, but unbounded process memory is still an availability risk for unattended Raspberry Pi deployments.
This is distinct from closed #228, which bounds EventBus stream queues, and from #213, which proposes bounded replay cursors. It is the runtime configuration boundary of the existing live WebSocket queue.
Suggested fix direction
Validate the queue size before the WebSocket upgrade/subscription is established:
0, negatives, booleans, non-numeric strings, and non-finite numeric inputs with a deterministic configuration error or safe HTTP 500/400 policy;Prefer a small helper so the server-level configuration contract is directly unit-testable.
Acceptance criteria
app["callstack_ws_queue_size"] = 0and negative values cannot create an unbounded queue._enqueue_websocket_envelope()in isolation.Verification gates
Duplicate check
Searched open/closed issues and PRs for
callstack_ws_queue_size,WebSocket queue maxsize zero,WebSocket slow client queue bound, andqueue size configuration PII websocket. The only adjacent work found was closed #228 (EventBus stream bounds) and open #213 (replay cursors); neither tracks an invalid live WebSocket queue override creatingasyncio.Queue(maxsize=0).