You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement room membership DoS protection with per-client room limits, per-room capacity caps, total room count ceiling, and resource-pressure circuit breaker
Difficulty
10/10 — Expert. Estimated effort: 3–4 days for a senior engineer.
Context
RoomManager.join() in src/room-manager.js (lines 51–58) accepts any clientId into any roomId with no limits whatsoever. A single misbehaving client can call join_room for 100,000 different room IDs, creating 100,000 entries in _rooms and 100,000 entries in _clientRooms. Each room entry is a Map<string, WebSocket> (minimum ~100 bytes overhead per Map), and each client-room entry is a Set<string> that grows by one per join. At 100K rooms, this is ~10MB of pure overhead — and that's from a single client.
Conversely, a single room can accumulate unlimited members. A public "region-global" room could attract 50,000 clients, making every broadcast() call iterate 50,000 WebSocket objects — an O(N) event-loop-blocking operation that takes ~50ms per broadcast at 50K members (issue 4 addresses broadcast backpressure, but the root cause is the lack of room capacity limits).
The project.md describes the system as supporting "fleet tracking, asset monitoring, geofencing enforcement, and live mapping." In a real deployment, rooms correspond to fleet IDs, regions, or user groups — all of which have natural cardinality and membership bounds. The absence of these bounds makes the system trivially vulnerable to resource exhaustion.
Problem statement
Implement a multi-layered resource accounting and DoS protection system for RoomManager:
Per-client room limit: A single client cannot join more than maxRoomsPerClient rooms (configurable, default 50). Excess join_room attempts return { type: "error", payload: { code: "ROOM_LIMIT_EXCEEDED", message: "..." } }.
Per-room member limit: A single room cannot have more than maxMembersPerRoom clients (configurable, default 10,000). Excess joins return { type: "error", payload: { code: "ROOM_FULL", message: "..." } }.
Total room count ceiling: The RoomManager cannot hold more than maxRooms rooms (configurable, default 10,000). When the ceiling is hit, new room creation (first join to a non-existent room) is rejected with { type: "error", payload: { code: "MAX_ROOMS_REACHED", message: "..." } }.
Resource-pressure circuit breaker: When total memory usage of the RoomManager (estimated via roomCount × avgRoomSize + clientCount × avgClientRoomCount) exceeds a configurable threshold, the circuit breaker opens. While open, all join_room operations are rejected with { type: "error", payload: { code: "CIRCUIT_BREAKER_OPEN", message: "..." } }. The breaker closes automatically when memory pressure subsides (configurable recovery threshold).
join(clientId,roomId,ws){if(clientId==null)thrownewTypeError("clientId is required");if(roomId==null)thrownewTypeError("roomId is required");if(ws==null)thrownewTypeError("ws is required");this._ensureRoom(roomId).set(clientId,ws);this._ensureClientRooms(clientId).add(roomId);}
No limits. No resource accounting. No circuit breaker. A single client can join unlimited rooms, and a single room can hold unlimited members.
join() checks all limits before adding membership and returns/throws structured error objects (not just TypeError).
The circuit breaker uses process.memoryUsage().heapUsed to estimate memory pressure.
All existing room-manager.test.js, room-manager-extended.test.js, room-manager-additional.test.js tests pass (they don't hit any limits with default settings).
Do not change the join(clientId, roomId, ws) method signature — callers pass the same arguments.
join() must throw TypeError for null/undefined required args (existing behavior) AND return/throw structured errors for limit violations.
Circuit breaker must not add per-request overhead beyond a single process.memoryUsage() call (which is ~1μs in Node.js).
Do not add new npm dependencies.
Do not modify server.js or any test file (except adding new test files).
The maxMembersPerRoom limit must be enforced atomically — no TOCTOU race between checking the count and adding the member (Node.js single-threaded, so this is naturally satisfied, but the code must not yield between check and insert).
Acceptance criteria
join("c1", "r1", ws) succeeds when client has < maxRoomsPerClient rooms
join("c1", "r2", ws) throws/returns error when client already has maxRoomsPerClient rooms
join("c1", "r1", ws) succeeds when room has < maxMembersPerRoom members
join("c2", "r1", ws) throws/returns error when room already has maxMembersPerRoom members
First join to a new room succeeds when roomCount < maxRooms
First join to a new room fails when roomCount === maxRooms (existing room joins still work)
Circuit breaker opens when heapUsed > memoryThresholdBytes
Circuit breaker closes when heapUsed < recoveryThresholdBytes
When circuit breaker is open, all join() calls are rejected
Changes to server.js, auth.js, validator.js, rate-limiter.js, index.js, logger.js.
Implementing per-room message rate limiting (that is a broadcast concern).
Dynamic limit adjustment based on system load (static limits are sufficient for this issue).
Hints and references
process.memoryUsage().heapUsed returns the V8 heap usage in bytes. It's fast (~1μs) but not free — call it only on join(), not on every broadcast().
The circuit breaker pattern has three states: CLOSED (normal), OPEN (rejecting), HALF_OPEN (testing recovery). For this implementation, CLOSED and OPEN are sufficient — the breaker transitions directly from OPEN to CLOSED when memory drops below the recovery threshold.
For maxMembersPerRoom, the check is simply room.size >= maxMembersPerRoom after _ensureRoom(roomId) — the room is guaranteed to exist at that point.
For maxRoomsPerClient, check _clientRooms.get(clientId)?.size ?? 0 before _ensureClientRooms(clientId).add(roomId).
For maxRooms, check _rooms.size before _ensureRoom(roomId) — but only when the room doesn't already exist: !this._rooms.has(roomId) && this._rooms.size >= maxRooms.
Title
Implement room membership DoS protection with per-client room limits, per-room capacity caps, total room count ceiling, and resource-pressure circuit breaker
Difficulty
10/10 — Expert. Estimated effort: 3–4 days for a senior engineer.
Context
RoomManager.join()insrc/room-manager.js(lines 51–58) accepts anyclientIdinto anyroomIdwith no limits whatsoever. A single misbehaving client can calljoin_roomfor 100,000 different room IDs, creating 100,000 entries in_roomsand 100,000 entries in_clientRooms. Each room entry is aMap<string, WebSocket>(minimum ~100 bytes overhead per Map), and each client-room entry is aSet<string>that grows by one per join. At 100K rooms, this is ~10MB of pure overhead — and that's from a single client.Conversely, a single room can accumulate unlimited members. A public "region-global" room could attract 50,000 clients, making every
broadcast()call iterate 50,000 WebSocket objects — an O(N) event-loop-blocking operation that takes ~50ms per broadcast at 50K members (issue 4 addresses broadcast backpressure, but the root cause is the lack of room capacity limits).The
project.mddescribes the system as supporting "fleet tracking, asset monitoring, geofencing enforcement, and live mapping." In a real deployment, rooms correspond to fleet IDs, regions, or user groups — all of which have natural cardinality and membership bounds. The absence of these bounds makes the system trivially vulnerable to resource exhaustion.Problem statement
Implement a multi-layered resource accounting and DoS protection system for
RoomManager:maxRoomsPerClientrooms (configurable, default 50). Excessjoin_roomattempts return{ type: "error", payload: { code: "ROOM_LIMIT_EXCEEDED", message: "..." } }.maxMembersPerRoomclients (configurable, default 10,000). Excess joins return{ type: "error", payload: { code: "ROOM_FULL", message: "..." } }.RoomManagercannot hold more thanmaxRoomsrooms (configurable, default 10,000). When the ceiling is hit, new room creation (firstjointo a non-existent room) is rejected with{ type: "error", payload: { code: "MAX_ROOMS_REACHED", message: "..." } }.RoomManager(estimated viaroomCount × avgRoomSize + clientCount × avgClientRoomCount) exceeds a configurable threshold, the circuit breaker opens. While open, alljoin_roomoperations are rejected with{ type: "error", payload: { code: "CIRCUIT_BREAKER_OPEN", message: "..." } }. The breaker closes automatically when memory pressure subsides (configurable recovery threshold).RoomManagerexposesstatsgetter returning{ roomCount, clientCount, totalMembers, circuitBreakerState }.Current behavior
src/room-manager.jsjoin()(lines 51–58):No limits. No resource accounting. No circuit breaker. A single client can join unlimited rooms, and a single room can hold unlimited members.
Required behavior
RoomManagerconstructor accepts{ maxRoomsPerClient, maxMembersPerRoom, maxRooms, circuitBreaker: { enabled, memoryThresholdBytes, recoveryThresholdBytes } }with sane defaults.join()checks all limits before adding membership and returns/throws structured error objects (not justTypeError).process.memoryUsage().heapUsedto estimate memory pressure.room-manager.test.js,room-manager-extended.test.js,room-manager-additional.test.jstests pass (they don't hit any limits with default settings).Constraints
join(clientId, roomId, ws)method signature — callers pass the same arguments.join()must throwTypeErrorfor null/undefined required args (existing behavior) AND return/throw structured errors for limit violations.process.memoryUsage()call (which is ~1μs in Node.js).server.jsor any test file (except adding new test files).maxMembersPerRoomlimit must be enforced atomically — no TOCTOU race between checking the count and adding the member (Node.js single-threaded, so this is naturally satisfied, but the code must not yield between check and insert).Acceptance criteria
join("c1", "r1", ws)succeeds when client has <maxRoomsPerClientroomsjoin("c1", "r2", ws)throws/returns error when client already hasmaxRoomsPerClientroomsjoin("c1", "r1", ws)succeeds when room has <maxMembersPerRoommembersjoin("c2", "r1", ws)throws/returns error when room already hasmaxMembersPerRoommembersroomCount < maxRoomsroomCount === maxRooms(existing room joins still work)heapUsed > memoryThresholdBytesheapUsed < recoveryThresholdBytesjoin()calls are rejectedstatsgetter returns{ roomCount, clientCount, totalMembers, circuitBreakerState }npm run lintpassesOut of scope
server.js,auth.js,validator.js,rate-limiter.js,index.js,logger.js.Hints and references
process.memoryUsage().heapUsedreturns the V8 heap usage in bytes. It's fast (~1μs) but not free — call it only onjoin(), not on everybroadcast().CLOSED(normal),OPEN(rejecting),HALF_OPEN(testing recovery). For this implementation,CLOSEDandOPENare sufficient — the breaker transitions directly fromOPENtoCLOSEDwhen memory drops below the recovery threshold.maxMembersPerRoom, the check is simplyroom.size >= maxMembersPerRoomafter_ensureRoom(roomId)— the room is guaranteed to exist at that point.maxRoomsPerClient, check_clientRooms.get(clientId)?.size ?? 0before_ensureClientRooms(clientId).add(roomId).maxRooms, check_rooms.sizebefore_ensureRoom(roomId)— but only when the room doesn't already exist:!this._rooms.has(roomId) && this._rooms.size >= maxRooms.