From 06960f1c38067a0656f293baee6509f9366f3d78 Mon Sep 17 00:00:00 2001 From: Albert Bausili Date: Sun, 18 Oct 2026 12:21:22 +0200 Subject: [PATCH 1/4] docs: the native engines close a detached connection whose peer takes none of its queued bytes for WriteTimeout (Refs celeris#888 items 36, 37; Refs celeris#884 item 66) --- src/content/docs/configuration.md | 24 ++++++++++++------------ src/content/docs/sse.md | 31 +++++++++++++++++++++---------- src/content/docs/streaming.md | 17 ++++++++++------- src/content/docs/websocket.md | 1 + 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/content/docs/configuration.md b/src/content/docs/configuration.md index df77c42..38d8a3d 100644 --- a/src/content/docs/configuration.md +++ b/src/content/docs/configuration.md @@ -205,7 +205,7 @@ inside their H1 header read loop (`celeris/config.go:94-104`, An SSE stream is a **detached** stream (the middleware calls [`Context.Detach`](/docs/streaming#detach)), and a detached stream is not subject to -`ReadTimeout` or `WriteTimeout` on any engine +`ReadTimeout` or `WriteTimeout` as a whole on any engine ([celeris#855](https://github.com/goceleris/celeris/issues/855)). You do not need `WriteTimeout: -1` for SSE, and a server-wide `WriteTimeout` of 30s or 60s does not end a stream: @@ -213,18 +213,17 @@ end a stream: | Engine | What a detached stream (SSE) sees | | ------ | --------------------------------- | | `std` | `net/http` arms one write deadline for the whole response, and `Detach` clears it for that request. Before celeris#855 it stayed armed and cut the stream at `WriteTimeout`, over HTTP/1.1 (the next `Send` failed) and over h2c (the stream was reset with `INTERNAL_ERROR`). `ReadTimeout` never cut a stream on Go 1.27: `net/http` has stopped it by the time the handler runs. In its place each write of the detached stream (a `Write` or `Flush`) is bounded by `WriteTimeout`: one that has not finished within it fails. For an SSE event, which is small, that means the client has stopped reading; a stream that is idle between events is not cut, and `-1` sets no such bound. | -| `epoll`, `io_uring`, `adaptive` | Neither timeout is applied to a detached connection. | +| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly, and a connection with nothing queued are not closed; `-1` sets no such bound ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37). | What bounds a detached stream is what its owner sets: the handler returning, the -client closing the connection, a shutdown. On `std` a client that stays connected but -stops reading ends its stream too: a write that has not finished within -`WriteTimeout` fails (the handler, or the queue's drain goroutine, is parked in it, so -returning or `ClientPolicyClose` cannot end the stream sooner), and when +client closing the connection, a shutdown. A client that stays connected but stops +reading ends its stream too, on every engine. On `std` a write that has not finished +within `WriteTimeout` fails (the handler, or the queue's drain goroutine, is parked in +it, so returning or `ClientPolicyClose` cannot end the stream sooner), and when `ShutdownTimeout` runs out the writes of the streams still running fail. On the native -engines `Send` was measured not to wait for a client that reads nothing, so the handler -returning ends the stream, and a shutdown does; neither `WriteTimeout` nor -`ClientPolicyClose` ends the connection of such a client yet (known gap, celeris#884 and -celeris#885). +engines `Send` does not wait for a client that reads nothing, so there it is the engine +that closes the connection once the client has taken none of the bytes queued for it for +`WriteTimeout`. A response that is **not** detached is still under `WriteTimeout`: a slow large download, or a chunked body written with `StreamWriter` without `Detach`. On `std` the @@ -664,5 +663,6 @@ So Celeris can tell "unset" (infer from `Protocol`) apart from an explicit - [Routing](/docs/routing) — per-route `Async` / `Sync` / `UsesDriver` overrides of `AsyncHandlers`. - [Streaming](/docs/streaming) and [SSE](/docs/sse) — long-lived responses: a detached - stream such as SSE is not subject to `ReadTimeout` or `WriteTimeout`; other - long-lived responses are. + stream such as SSE is not subject to `ReadTimeout` or `WriteTimeout` as a whole (a + client that reads nothing of it is closed after `WriteTimeout`); other long-lived + responses are. diff --git a/src/content/docs/sse.md b/src/content/docs/sse.md index 6427723..4d10f00 100644 --- a/src/content/docs/sse.md +++ b/src/content/docs/sse.md @@ -564,18 +564,29 @@ why the same `sse.Config` runs unchanged on all engines. Source: ### Server timeouts An SSE stream is detached, so the server's `ReadTimeout` and `WriteTimeout` do not end -it on any engine: leave them at their defaults and do not set `WriteTimeout: -1` for +it as a whole on any engine: leave them at their defaults and do not set `WriteTimeout: -1` for SSE. On `std` the `Detach` clears the write deadline `net/http` armed for the request (before [celeris#855](https://github.com/goceleris/celeris/issues/855) it cut the stream -at `WriteTimeout`; `ReadTimeout` never did); the native engines apply neither to a -detached connection. On `std` a write of the stream that has not finished within -`WriteTimeout` fails (no bound at `-1`), so a client that stays connected and reads nothing -ends its stream after one `WriteTimeout` (the handler is parked in the write until -then), and a shutdown whose budget runs out fails the writes of the streams still -running. On the native engines `Send` does not wait for such a client; the handler -returning or a shutdown ends the stream, `WriteTimeout` and `ClientPolicyClose` do not -yet (celeris#884, celeris#885). A stream is ended by its handler returning, the client -closing the connection, or a shutdown. See [Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). +at `WriteTimeout`; `ReadTimeout` never did). A client that stays connected and stops +reading is dealt with per engine: + +- **`std`**: a write of the stream that has not finished within `WriteTimeout` fails (no + bound at `-1`), so the client's stream ends after one `WriteTimeout` (the handler is + parked in the write until then), and a shutdown whose budget runs out fails the writes + of the streams still running. +- **`epoll`, `io_uring`, `adaptive`**: `Send` does not wait for such a client (the write + only queues the event in the engine), so a write error and `ClientPolicyClose` are not + what ends its stream. `WriteTimeout` does, as "no progress for `WriteTimeout`": a client + that takes none of the bytes the engine has queued for it for `WriteTimeout` is + disconnected, its context is cancelled and `OnDisconnect` runs + ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37). A client + that keeps reading, however slowly, is not disconnected, and neither is one with nothing + queued for it; `-1` sets no such bound. The handler returning or a shutdown ends the + stream sooner. + +A stream is ended by its handler returning, the client closing the connection, a shutdown, +or, on every engine, a client that stops reading. See +[Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). ## Common pitfalls diff --git a/src/content/docs/streaming.md b/src/content/docs/streaming.md index 2ab02ad..718eb4f 100644 --- a/src/content/docs/streaming.md +++ b/src/content/docs/streaming.md @@ -188,13 +188,16 @@ returns a `done func()`. Source: `celeris/context_response.go:1471`. After `Detach`, the framework will not recycle the `Context` when the handler returns; the returned `done` is what releases it. -A detached stream is also exempt from the server's `ReadTimeout` and `WriteTimeout` on -every engine ([celeris#855](https://github.com/goceleris/celeris/issues/855)): `epoll`, -`io_uring` and `adaptive` apply neither to a detached connection, and on `std` `Detach` -clears the write deadline `net/http` armed for the request and bounds each write of the -stream by `WriteTimeout` instead (a write that has not finished within `WriteTimeout` -fails, so a client that reads nothing ends the stream; a shutdown whose budget runs out -fails the writes). An undetached +A detached stream is also exempt from the server's `ReadTimeout` and `WriteTimeout` as a +whole on every engine ([celeris#855](https://github.com/goceleris/celeris/issues/855)): +`epoll`, `io_uring` and `adaptive` apply `ReadTimeout` and `IdleTimeout` to a detached +connection not at all, and `WriteTimeout` as "no progress for `WriteTimeout`" (a connection +with bytes queued for a peer that takes none of them for `WriteTimeout` is closed; +[celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37), and on +`std` `Detach` clears the write deadline `net/http` armed for the request and bounds each +write of the stream by `WriteTimeout` instead (a write that has not finished within +`WriteTimeout` fails, so a client that reads nothing ends the stream; a shutdown whose +budget runs out fails the writes). An undetached `StreamWriter` is still a response with a `WriteTimeout`. See [Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). diff --git a/src/content/docs/websocket.md b/src/content/docs/websocket.md index 9cb2cf5..018bed1 100644 --- a/src/content/docs/websocket.md +++ b/src/content/docs/websocket.md @@ -710,6 +710,7 @@ worth knowing (`celeris/middleware/websocket/doc.go:41-44`): | `IdleTimeout` | Enforced via the engine's idle sweep. | Enforced via `net.Conn.SetReadDeadline`. | | Backpressure | Engine-integrated (TCP receive window); see [Backpressure](#backpressure-native-engines). | Handled by the kernel TCP stack. | | `WriteBufferPool` | Ignored (engine pools its own write buffer). | Consulted. | +| Peer that reads nothing | The server's `WriteTimeout` closes the connection once the peer has taken none of the bytes queued for it for `WriteTimeout` ("no progress", whatever the peer sends meanwhile; `-1` sets no such bound; [celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37). | Not changed by celeris#888. | The practical takeaways: From 98ac8e12e6d6295b10fe7d6e9cc344279b6ac881 Mon Sep 17 00:00:00 2001 From: Albert Bausili Date: Sun, 18 Oct 2026 16:16:18 +0200 Subject: [PATCH 2/4] docs: the checklist items are celeris#888 items 38 and 39 and #884 item 69; ClientPolicyClose can end a stalled stream earlier; no minimum rate; h2c is not covered yet (Refs celeris#888 items 38, 39; Refs celeris#884 item 69) --- src/content/docs/configuration.md | 4 ++-- src/content/docs/sse.md | 12 +++++++----- src/content/docs/streaming.md | 2 +- src/content/docs/websocket.md | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/content/docs/configuration.md b/src/content/docs/configuration.md index 38d8a3d..38344e8 100644 --- a/src/content/docs/configuration.md +++ b/src/content/docs/configuration.md @@ -213,11 +213,11 @@ end a stream: | Engine | What a detached stream (SSE) sees | | ------ | --------------------------------- | | `std` | `net/http` arms one write deadline for the whole response, and `Detach` clears it for that request. Before celeris#855 it stayed armed and cut the stream at `WriteTimeout`, over HTTP/1.1 (the next `Send` failed) and over h2c (the stream was reset with `INTERNAL_ERROR`). `ReadTimeout` never cut a stream on Go 1.27: `net/http` has stopped it by the time the handler runs. In its place each write of the detached stream (a `Write` or `Flush`) is bounded by `WriteTimeout`: one that has not finished within it fails. For an SSE event, which is small, that means the client has stopped reading; a stream that is idle between events is not cut, and `-1` sets no such bound. | -| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly, and a connection with nothing queued are not closed; `-1` sets no such bound ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37). | +| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly (there is no minimum rate), and a connection with nothing queued are not closed; `-1` sets no such bound ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | What bounds a detached stream is what its owner sets: the handler returning, the client closing the connection, a shutdown. A client that stays connected but stops -reading ends its stream too, on every engine. On `std` a write that has not finished +reading ends its stream too, on HTTP/1.1 on every engine (an h2c stream is not covered yet). On `std` a write that has not finished within `WriteTimeout` fails (the handler, or the queue's drain goroutine, is parked in it, so returning or `ClientPolicyClose` cannot end the stream sooner), and when `ShutdownTimeout` runs out the writes of the streams still running fail. On the native diff --git a/src/content/docs/sse.md b/src/content/docs/sse.md index 4d10f00..1a78ad1 100644 --- a/src/content/docs/sse.md +++ b/src/content/docs/sse.md @@ -575,17 +575,19 @@ reading is dealt with per engine: parked in the write until then), and a shutdown whose budget runs out fails the writes of the streams still running. - **`epoll`, `io_uring`, `adaptive`**: `Send` does not wait for such a client (the write - only queues the event in the engine), so a write error and `ClientPolicyClose` are not - what ends its stream. `WriteTimeout` does, as "no progress for `WriteTimeout`": a client + only queues the event in the engine), so a write error is not what ends its stream, and + `ClientPolicyClose` ends it only earlier, when the queue overflows (a drain goroutine + that lags behind the producer). `WriteTimeout` is the bound when it does not, as "no + progress for `WriteTimeout`": a client that takes none of the bytes the engine has queued for it for `WriteTimeout` is disconnected, its context is cancelled and `OnDisconnect` runs - ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37). A client - that keeps reading, however slowly, is not disconnected, and neither is one with nothing + ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). A client + that keeps reading, however slowly (there is no minimum rate), is not disconnected, and neither is one with nothing queued for it; `-1` sets no such bound. The handler returning or a shutdown ends the stream sooner. A stream is ended by its handler returning, the client closing the connection, a shutdown, -or, on every engine, a client that stops reading. See +or a client that stops reading (HTTP/1.1 on every engine; an h2c stream is not covered yet). See [Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). ## Common pitfalls diff --git a/src/content/docs/streaming.md b/src/content/docs/streaming.md index 718eb4f..119d8c4 100644 --- a/src/content/docs/streaming.md +++ b/src/content/docs/streaming.md @@ -193,7 +193,7 @@ whole on every engine ([celeris#855](https://github.com/goceleris/celeris/issues `epoll`, `io_uring` and `adaptive` apply `ReadTimeout` and `IdleTimeout` to a detached connection not at all, and `WriteTimeout` as "no progress for `WriteTimeout`" (a connection with bytes queued for a peer that takes none of them for `WriteTimeout` is closed; -[celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37), and on +[celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39), and on `std` `Detach` clears the write deadline `net/http` armed for the request and bounds each write of the stream by `WriteTimeout` instead (a write that has not finished within `WriteTimeout` fails, so a client that reads nothing ends the stream; a shutdown whose diff --git a/src/content/docs/websocket.md b/src/content/docs/websocket.md index 018bed1..c7f53dd 100644 --- a/src/content/docs/websocket.md +++ b/src/content/docs/websocket.md @@ -710,7 +710,7 @@ worth knowing (`celeris/middleware/websocket/doc.go:41-44`): | `IdleTimeout` | Enforced via the engine's idle sweep. | Enforced via `net.Conn.SetReadDeadline`. | | Backpressure | Engine-integrated (TCP receive window); see [Backpressure](#backpressure-native-engines). | Handled by the kernel TCP stack. | | `WriteBufferPool` | Ignored (engine pools its own write buffer). | Consulted. | -| Peer that reads nothing | The server's `WriteTimeout` closes the connection once the peer has taken none of the bytes queued for it for `WriteTimeout` ("no progress", whatever the peer sends meanwhile; `-1` sets no such bound; [celeris#888](https://github.com/goceleris/celeris/issues/888) items 36 and 37). | Not changed by celeris#888. | +| Peer that reads nothing | The server's `WriteTimeout` closes the connection once the peer has taken none of the bytes queued for it for `WriteTimeout` ("no progress", whatever the peer sends meanwhile; `-1` sets no such bound; [celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | Not changed by celeris#888. | The practical takeaways: From ee93f257e2bf62b8ee573a1d5700af8153009534 Mon Sep 17 00:00:00 2001 From: Albert Bausili Date: Sun, 18 Oct 2026 16:31:14 +0200 Subject: [PATCH 3/4] docs: the disconnect comes WriteTimeout after the last byte the client took, plus the sweep interval (Refs celeris#888 items 38, 39) --- src/content/docs/configuration.md | 2 +- src/content/docs/sse.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/content/docs/configuration.md b/src/content/docs/configuration.md index 38344e8..2771919 100644 --- a/src/content/docs/configuration.md +++ b/src/content/docs/configuration.md @@ -213,7 +213,7 @@ end a stream: | Engine | What a detached stream (SSE) sees | | ------ | --------------------------------- | | `std` | `net/http` arms one write deadline for the whole response, and `Detach` clears it for that request. Before celeris#855 it stayed armed and cut the stream at `WriteTimeout`, over HTTP/1.1 (the next `Send` failed) and over h2c (the stream was reset with `INTERNAL_ERROR`). `ReadTimeout` never cut a stream on Go 1.27: `net/http` has stopped it by the time the handler runs. In its place each write of the detached stream (a `Write` or `Flush`) is bounded by `WriteTimeout`: one that has not finished within it fails. For an SSE event, which is small, that means the client has stopped reading; a stream that is idle between events is not cut, and `-1` sets no such bound. | -| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly (there is no minimum rate), and a connection with nothing queued are not closed; `-1` sets no such bound ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | +| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly (there is no minimum rate), and a connection with nothing queued are not closed; `-1` sets no such bound. The close comes `WriteTimeout` after the peer last took a byte, plus the engine's sweep interval (about 50 ms on `epoll`, up to 2 s on `io_uring`; a `WriteTimeout` shorter than that can double) ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | What bounds a detached stream is what its owner sets: the handler returning, the client closing the connection, a shutdown. A client that stays connected but stops diff --git a/src/content/docs/sse.md b/src/content/docs/sse.md index 1a78ad1..7530fb1 100644 --- a/src/content/docs/sse.md +++ b/src/content/docs/sse.md @@ -583,7 +583,9 @@ reading is dealt with per engine: disconnected, its context is cancelled and `OnDisconnect` runs ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). A client that keeps reading, however slowly (there is no minimum rate), is not disconnected, and neither is one with nothing - queued for it; `-1` sets no such bound. The handler returning or a shutdown ends the + queued for it; `-1` sets no such bound. The disconnect comes `WriteTimeout` after the + client last took a byte, plus the engine's sweep interval (about 50 ms on `epoll`, up to + 2 s on `io_uring`; a `WriteTimeout` shorter than that can double). The handler returning or a shutdown ends the stream sooner. A stream is ended by its handler returning, the client closing the connection, a shutdown, From 96391d775b7d01f679923053a2ca123e492c1982 Mon Sep 17 00:00:00 2001 From: Albert Bausili Date: Sun, 18 Oct 2026 17:06:05 +0200 Subject: [PATCH 4/4] docs: the stall close comes between 3/4 and 5/4 of WriteTimeout after the last byte, plus the sweep interval (Refs celeris#888 items 38, 39) --- src/content/docs/configuration.md | 2 +- src/content/docs/sse.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/content/docs/configuration.md b/src/content/docs/configuration.md index 2771919..a37d4b0 100644 --- a/src/content/docs/configuration.md +++ b/src/content/docs/configuration.md @@ -213,7 +213,7 @@ end a stream: | Engine | What a detached stream (SSE) sees | | ------ | --------------------------------- | | `std` | `net/http` arms one write deadline for the whole response, and `Detach` clears it for that request. Before celeris#855 it stayed armed and cut the stream at `WriteTimeout`, over HTTP/1.1 (the next `Send` failed) and over h2c (the stream was reset with `INTERNAL_ERROR`). `ReadTimeout` never cut a stream on Go 1.27: `net/http` has stopped it by the time the handler runs. In its place each write of the detached stream (a `Write` or `Flush`) is bounded by `WriteTimeout`: one that has not finished within it fails. For an SSE event, which is small, that means the client has stopped reading; a stream that is idle between events is not cut, and `-1` sets no such bound. | -| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly (there is no minimum rate), and a connection with nothing queued are not closed; `-1` sets no such bound. The close comes `WriteTimeout` after the peer last took a byte, plus the engine's sweep interval (about 50 ms on `epoll`, up to 2 s on `io_uring`; a `WriteTimeout` shorter than that can double) ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | +| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly (there is no minimum rate), and a connection with nothing queued are not closed; `-1` sets no such bound. The close comes between about 3/4 and 5/4 of `WriteTimeout` after the peer last took a byte (its progress is sampled every quarter of `WriteTimeout`; a send completion restarts the clock at once), plus the engine's sweep interval (up to about 0.1 s on `epoll`, up to 2 s on `io_uring`; a `WriteTimeout` shorter than that can double) ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | What bounds a detached stream is what its owner sets: the handler returning, the client closing the connection, a shutdown. A client that stays connected but stops diff --git a/src/content/docs/sse.md b/src/content/docs/sse.md index 7530fb1..7a73ff2 100644 --- a/src/content/docs/sse.md +++ b/src/content/docs/sse.md @@ -583,9 +583,11 @@ reading is dealt with per engine: disconnected, its context is cancelled and `OnDisconnect` runs ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). A client that keeps reading, however slowly (there is no minimum rate), is not disconnected, and neither is one with nothing - queued for it; `-1` sets no such bound. The disconnect comes `WriteTimeout` after the - client last took a byte, plus the engine's sweep interval (about 50 ms on `epoll`, up to - 2 s on `io_uring`; a `WriteTimeout` shorter than that can double). The handler returning or a shutdown ends the + queued for it; `-1` sets no such bound. The disconnect comes between about 3/4 + and 5/4 of `WriteTimeout` after the client last took a byte (its progress is sampled every + quarter of `WriteTimeout`; a send completion restarts the clock at once), plus the + engine's sweep interval (up to about 0.1 s on `epoll`, up to 2 s on `io_uring`; a + `WriteTimeout` shorter than that can double). The handler returning or a shutdown ends the stream sooner. A stream is ended by its handler returning, the client closing the connection, a shutdown,