Skip to content

WebSocket Keep-Alive #44611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 31, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/fundamentals/networking/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,76 @@ using ClientWebSocket ws = new()

> [!IMPORTANT]
> Before using compression, please be aware that enabling it makes the application subject to CRIME/BREACH type of attacks, for more information, see [CRIME](https://en.wikipedia.org/wiki/CRIME) and [BREACH](https://en.wikipedia.org/wiki/BREACH). It is strongly advised to turn off compression when sending data containing secrets by specifying the `DisableCompression` flag for such messages.

## Keep-Alive strategies

On **.NET 8** and earlier, the only available Keep-Alive strategy is _Unsolicited PONG_. This strategy is enough to keep the underlying TCP connection from idling out. However, in a case when a remote host becomes unresponsive (for example, a remote server crashes), the only way to detect such situations with Unsolicited PONG is to depend on the TCP timeout.

**.NET 9** introduced the long-desired _PING/PONG_ Keep-Alive strategy, complementing the existing `KeepAliveInterval` setting with the new `KeepAliveTimeout` setting. Starting with .NET 9, the Keep-Alive strategy is selected as follows:

1. Keep-Alive is **OFF**, if
- `KeepAliveInterval` is `TimeSpan.Zero` or `Timeout.InfiniteTimeSpan`
2. **Unsolicited PONG**, if
- `KeepAliveInterval` is a positive finite `TimeSpan`, -AND-
- `KeepAliveTimeout` is `TimeSpan.Zero` or `Timeout.InfiniteTimeSpan`
3. **PING/PONG**, if
- `KeepAliveInterval` is a positive finite `TimeSpan`, -AND-
- `KeepAliveTimeout` is a positive finite `TimeSpan`
Comment on lines +109 to +116
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use <xref> here instead of code blocks.


The default `KeepAliveTimeout` value is `Timeout.InfiniteTimeSpan`, so the default Keep-Alive behavior remains consistent between .NET versions.

If you use `ClientWebSocket`, the default <xref:System.Net.WebSockets.ClientWebSocketOptions.KeepAliveInterval?displayProperty=nameWithType> value is <xref:System.Net.WebSockets.WebSocket.DefaultKeepAliveInterval?displayProperty=nameWithType> (typically 30 seconds). That means, `ClientWebSocket` has the Keep-Alive ON by default, with Unsolicited PONG as the default strategy.

If you want to switch to the PING/PONG strategy, overriding <xref:System.Net.WebSockets.ClientWebSocketOptions.KeepAliveTimeout?displayProperty=nameWithType> is enough:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good suggest that this is an implication of the behavior described before.

Suggested change
If you want to switch to the PING/PONG strategy, overriding <xref:System.Net.WebSockets.ClientWebSocketOptions.KeepAliveTimeout?displayProperty=nameWithType> is enough:
As a result, overriding <xref:System.Net.WebSockets.ClientWebSocketOptions.KeepAliveTimeout?displayProperty=nameWithType> is enough to switch to the PING/PONG strategy if you keep the default value for <xref:System.Net.WebSockets.WebSocket.DefaultKeepAliveInterval?displayProperty=nameWithType>:


```csharp
var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(20);
await ws.ConnectAsync(uri, cts.Token);
```

For a basic `WebSocket`, the Keep-Alive is OFF by default. If you want to use the PING/PONG strategy, both <xref:System.Net.WebSockets.WebSocketCreationOptions.KeepAliveInterval?displayProperty=nameWithType> and <xref:System.Net.WebSockets.WebSocketCreationOptions.KeepAliveTimeout?displayProperty=nameWithType> need to be set:

```csharp
var options = new WebSocketCreationOptions()
{
KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
KeepAliveTimeout = TimeSpan.FromSeconds(20)
};
var ws = WebSocket.CreateFromStream(stream, options);
```

If the Unsolicited PONG strategy is used, PONG frames are used as a unidirectional heartbeat. They sent regularly with `KeepAliveInterval` intervals, regardless whether the remote endpoint is communicating or not.

In case the PING/PONG strategy is active, a PING frame is sent after `KeepAliveInterval` time passed since the _last communication_ from the remote endpoint. Each PING frame contains an integer token to pair with the expected PONG response. If no PONG response arrived after `KeepAliveTimeout` elapsed, the remote endpoint is deemed unresponsive, and the WebSocket connection is automatically aborted.

```csharp
var ws = new ClientWebSocket();
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);

// NOTE: There must be an outstanding read at all times to ensure
// incoming PONGs are processed
var result = await _webSocket.ReceiveAsync(buffer, cts.Token);
```

If the timeout elapses, an outstanding `ReceiveAsync` throws an `OperationCanceledException`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If the timeout elapses, an outstanding `ReceiveAsync` throws an `OperationCanceledException`:
If the timeout elapses, an outstanding <xref:System.Net.WebSockets.WebSocket.ReceiveAsync%2A> throws an <xref:System.OperationCanceledException>:


```txt
System.OperationCanceledException: Aborted
---> System.AggregateException: One or more errors occurred. (The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.) (Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request..)
---> System.Net.WebSockets.WebSocketException (0x80004005): The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.
at System.Net.WebSockets.ManagedWebSocket.KeepAlivePingHeartBeat()
...
```

### Keep Reading To Process PONGs

> [!NOTE]
> Currently, `WebSocket` ONLY processes incoming frames while there's a `ReceiveAsync` pending.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> Currently, `WebSocket` ONLY processes incoming frames while there's a `ReceiveAsync` pending.
> Currently, `WebSocket` ONLY processes incoming frames while there's a <xref:System.Net.WebSockets.WebSocket.ReceiveAsync%2A> task pending.


> [!IMPORTANT]
> If you want to use Keep-Alive Timeout, it's _crucial_ that PONG responses are _promptly processed_. Even if the remote endpoint is alive and properly sends the PONG response, but the `WebSocket` isn't processing the incoming frames, the Keep-Alive mechanism can issue a "false-positive" Abort. This problem can happen if the PONG frame is never picked up from the transport stream before the timeout elapsed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> If you want to use Keep-Alive Timeout, it's _crucial_ that PONG responses are _promptly processed_. Even if the remote endpoint is alive and properly sends the PONG response, but the `WebSocket` isn't processing the incoming frames, the Keep-Alive mechanism can issue a "false-positive" Abort. This problem can happen if the PONG frame is never picked up from the transport stream before the timeout elapsed.
> If you want to use Keep-Alive Timeout, it's _crucial_ that PONG responses are _promptly processed_. Even when the remote endpoint is alive and properly sends the PONG response, if the `WebSocket` isn't processing the incoming frames, the Keep-Alive mechanism can issue a "false-positive" Abort. This problem can happen if the PONG frame is never picked up from the transport stream before the timeout elapsed.


To avoid tearing up good connections, users are advised to maintain a pending read on all WebSockets that have Keep-Alive Timeout configured.