Describe the bug
There is a critical resource leak in the WebSocket heartbeat mechanism located in src/adapters/web-socket-adapter.ts:132.
During the onHeartbeat() check, the code assumes that any client with an active subscription should bypass eviction: if (!this.alive && !this.subscriptions.size). However, if a client establishes a connection, submits a broad subscription REQ, and then silently drops off the network without sending a TCP FIN, it will never respond to pings. Due to this subscription check, the server ignores the ping timeout and leaves the socket metadata alive indefinitely.
Attackers can abuse this by opening thousands of "zombie" connections. The server will unnecessarily run filter matching and attempt to write new events down thousands of dead TCP sockets, causing an unnatural and immediate spike in CPU and Memory usage per broadcast.
As evidenced by the attached network/Docker statistics, running a PoC against a vulnerable relay spiked the CPU usage sharply, and the server was tricked into writing massive amounts of data out to completely unresponsive network interfaces over the span of just a few minutes.
To Reproduce
Steps to reproduce the behavior:
- Start the Nostream relay server locally.
- Write a script that connects to
ws://localhost:8008 5,000 times.
- For each connection, send a valid subscription REQ.
- Intentionally configure the client script to ignore all incoming server
ping frames and never respond with pong.
- Observe that the server does not evict the connections.
- Send a few valid Note events to the server and observe the massive spike in CPU/Memory usage as the server attempts to broadcast to the zombie sockets.
Expected behavior
The server should unconditionally terminate heavily-unresponsive WebSocket connections if they fail the heartbeat ping/pong cycle, regardless of whether they have active subscriptions or not.
The fix is to remove the active subscription check from line 132 in web-socket-adapter.ts:
if (!this.alive) {
Screenshots
Please find attached the Docker statistics representing the relay's performance both Without the attack active (normal baseline) and With the attack actively abusing the vulnerability. Note the massive, asymmetrical spikes in Memory usage and Network outbound I/O when the attack is active.
Without Script:

While running Script:
System (please complete the following information):
- OS: Linux
- Platform: Docker
- Version: 2.1.1 (or currently cloned main branch)
Logs
N/A
Additional context
Verified via local PoC script. Standard NodeJS ws connections are naturally lightweight, but the architectural flaw occurs when the Nostr Event Loop kicks in. An attacker doesn't even need to spam events themselves—they can simply open the zombie connections and let normal public relay traffic naturally starve the CPUs on the host system via dead broadcasts.
Describe the bug
There is a critical resource leak in the WebSocket heartbeat mechanism located in
src/adapters/web-socket-adapter.ts:132.During the onHeartbeat() check, the code assumes that any client with an active subscription should bypass eviction:
if (!this.alive && !this.subscriptions.size). However, if a client establishes a connection, submits a broad subscriptionREQ, and then silently drops off the network without sending a TCPFIN, it will never respond to pings. Due to this subscription check, the server ignores the ping timeout and leaves the socket metadata alive indefinitely.Attackers can abuse this by opening thousands of "zombie" connections. The server will unnecessarily run filter matching and attempt to write new events down thousands of dead TCP sockets, causing an unnatural and immediate spike in CPU and Memory usage per broadcast.
As evidenced by the attached network/Docker statistics, running a PoC against a vulnerable relay spiked the CPU usage sharply, and the server was tricked into writing massive amounts of data out to completely unresponsive network interfaces over the span of just a few minutes.
To Reproduce
Steps to reproduce the behavior:
ws://localhost:80085,000 times.pingframes and never respond withpong.Expected behavior
The server should unconditionally terminate heavily-unresponsive WebSocket connections if they fail the heartbeat ping/pong cycle, regardless of whether they have active subscriptions or not.
The fix is to remove the active subscription check from line 132 in web-socket-adapter.ts:
if (!this.alive) {Screenshots
Please find attached the Docker statistics representing the relay's performance both Without the attack active (normal baseline) and With the attack actively abusing the vulnerability. Note the massive, asymmetrical spikes in Memory usage and Network outbound I/O when the attack is active.
Without Script:

While running Script:
System (please complete the following information):
Logs
N/A
Additional context
Verified via local PoC script. Standard NodeJS
wsconnections are naturally lightweight, but the architectural flaw occurs when the Nostr Event Loop kicks in. An attacker doesn't even need to spam events themselves—they can simply open the zombie connections and let normal public relay traffic naturally starve the CPUs on the host system via dead broadcasts.