Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def hello(request):
app.start(
host="0.0.0.0",
port=8080,
client_timeout=30, # Client connection timeout (seconds)
client_timeout=30, # Max seconds to read request headers (seconds)
keep_alive_timeout=20 # Keep-alive timeout (seconds)
)
```
Expand All @@ -46,8 +46,8 @@ python app.py

| Parameter | Default | Description | Environment Variable |
|-----------|---------|-------------|---------------------|
| `client_timeout` | 30 | Maximum time (seconds) for client request processing | `ROBYN_CLIENT_TIMEOUT` |
| `keep_alive_timeout` | 20 | Time (seconds) to keep idle connections alive | `ROBYN_KEEP_ALIVE_TIMEOUT` |
| `client_timeout` | 30 | Maximum time (seconds) to wait for the client to transmit the complete request headers. This does **not** limit handler execution time or overall request duration — it only guards against slow or stalled clients during the initial header-reading phase. Maps to actix-web's `client_request_timeout`. | `ROBYN_CLIENT_TIMEOUT` |
| `keep_alive_timeout` | 20 | Time (seconds) to keep idle connections alive before closing them | `ROBYN_KEEP_ALIVE_TIMEOUT` |

## Usage Examples

Expand All @@ -65,7 +65,7 @@ app.start(client_timeout=30)
app.start(
host="0.0.0.0",
port=8080,
client_timeout=60, # Allow longer processing time
client_timeout=60, # More time for clients on slow networks to send headers
keep_alive_timeout=15 # Shorter keep-alive for faster turnover
)
```
Expand All @@ -75,7 +75,7 @@ app.start(
```python
# Development-friendly settings
app.start(
client_timeout=300, # Long timeout for debugging
client_timeout=300, # Generous header-read timeout for debugging
keep_alive_timeout=60 # Longer keep-alive for testing
)
```
Expand All @@ -85,7 +85,7 @@ app.start(
```python
# Optimized for load testing with tools like wrk
app.start(
client_timeout=10, # Quick timeouts
client_timeout=10, # Quick header-read timeout
keep_alive_timeout=5 # Fast connection turnover
)
```
Expand Down Expand Up @@ -130,10 +130,12 @@ If you encounter file descriptor exhaustion:
- Lower `keep_alive_timeout` (5-15s)
- Moderate `client_timeout` (15-30s)

**For long-running operations:**
- Higher `client_timeout` (60-300s)
**For clients on slow networks:**
- Higher `client_timeout` (60-120s) so slow clients have time to send headers
- Standard `keep_alive_timeout` (20-30s)

> **Note:** `client_timeout` only controls how long the server waits for request headers — it does not cap handler execution time. If you need to limit how long a handler can run, implement that logic within the handler itself (e.g. with `asyncio.wait_for`).


## Best Practices

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def hello(request):
app.start(
host="0.0.0.0",
port=8080,
client_timeout=30, # 客户端连接超时(秒)
client_timeout=30, # 读取请求头的最大等待时间(秒)
keep_alive_timeout=20, # 保持连接超时(秒)
)
```
Expand All @@ -46,7 +46,7 @@ python app.py

| 参数 | 默认值 | 描述 | 环境变量 |
|------|-------|------|---------|
| `client_timeout` | 30 | 客户端请求处理的最大时间(秒) | `ROBYN_CLIENT_TIMEOUT` |
| `client_timeout` | 30 | 等待客户端发送完整请求头的最大时间(秒)。此参数**不**限制处理函数执行时间或整个请求的持续时间——它仅防止客户端在发送头信息阶段过慢或停滞。对应 actix-web 的 `client_request_timeout`。 | `ROBYN_CLIENT_TIMEOUT` |
| `keep_alive_timeout` | 20 | 保持空闲连接的时间(秒) | `ROBYN_KEEP_ALIVE_TIMEOUT` |

## 使用示例
Expand All @@ -65,7 +65,7 @@ app.start(client_timeout=30)
app.start(
host="0.0.0.0",
port=8080,
client_timeout=60, # 允许更长的处理时间
client_timeout=60, # 为慢速网络客户端留更多时间发送请求头
keep_alive_timeout=15, # 更短的保持连接以加快周转
)
```
Expand All @@ -75,7 +75,7 @@ app.start(
```python
# 开发友好设置
app.start(
client_timeout=300, # 调试的长超时
client_timeout=300, # 调试时宽裕的请求头读取超时
keep_alive_timeout=60, # 测试的长保持连接
)
```
Expand All @@ -85,7 +85,7 @@ app.start(
```python
# 针对 wrk 等工具的负载测试优化
app.start(
client_timeout=10, # 快速超时
client_timeout=10, # 快速请求头读取超时
keep_alive_timeout=5, # 快速连接周转
)
```
Expand Down Expand Up @@ -130,10 +130,12 @@ app.start(client_timeout=30)
- 较低的 `keep_alive_timeout` (5-15秒)
- 中等的 `client_timeout` (15-30秒)

**对于长时间运行的操作:**
- 较高的 `client_timeout` (60-300秒)
**对于慢速网络上的客户端:**
- 较高的 `client_timeout` (60-120秒),为慢速客户端留出发送请求头的时间
- 标准的 `keep_alive_timeout` (20-30秒)

> **注意:** `client_timeout` 仅控制服务器等待请求头的时间——它不会限制处理函数的执行时间。如需限制处理函数运行时长,请在处理函数内部实现该逻辑(例如使用 `asyncio.wait_for`)。

## 与其他框架的比较

| 框架 | 默认客户端超时 | 默认保持连接 | 配置方式 |
Expand Down
11 changes: 9 additions & 2 deletions robyn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,15 @@ def start(self, host: str = "127.0.0.1", port: int = 8080, _check_port: bool = T
:param host str: represents the host at which the server is listening
:param port int: represents the port number at which the server is listening
:param _check_port bool: represents if the port should be checked if it is already in use
:param client_timeout int: timeout for client connections in seconds (default: 30)
:param keep_alive_timeout int: timeout for keep-alive connections in seconds (default: 20)
:param client_timeout int: maximum time in seconds to wait for the client to send
the complete request headers (default: 30). This does **not** limit handler
execution time or the overall request duration — it only guards against slow
or stalled clients during the initial header-reading phase. Under the hood
this maps to actix-web's ``client_request_timeout``.
Can be overridden with the ``ROBYN_CLIENT_TIMEOUT`` environment variable.
:param keep_alive_timeout int: time in seconds to keep an idle connection open
before closing it (default: 20). Can be overridden with the
``ROBYN_KEEP_ALIVE_TIMEOUT`` environment variable.
"""

host = os.getenv("ROBYN_HOST", host)
Expand Down
16 changes: 15 additions & 1 deletion robyn/processpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ def run_processes(
client_timeout: int = 30,
keep_alive_timeout: int = 20,
) -> List[Process]:
"""
Spawn server processes.

:param client_timeout: max seconds to wait for the client to send the
complete request headers (maps to actix-web's
``client_request_timeout``). This does **not** limit handler execution
time or overall request duration.
:param keep_alive_timeout: seconds to keep idle connections open.
"""
socket = SocketHeld(url, port)

process_pool = init_processpool(
Expand Down Expand Up @@ -175,6 +184,11 @@ def spawn_process(
:param socket SocketHeld: This is the main tcp socket, which is being shared across multiple processes.
:param process_name string: This is the name given to the process to identify the process
:param workers int: This is the name given to the process to identify the process
:param client_timeout int: max seconds to wait for the client to send the
complete request headers (maps to actix-web's
``client_request_timeout``). This does **not** limit handler execution
time or overall request duration.
:param keep_alive_timeout int: seconds to keep idle connections open.
"""

loop = initialize_event_loop()
Expand Down Expand Up @@ -218,7 +232,7 @@ def spawn_process(
)

try:
server.start(socket, workers)
server.start(socket, workers, client_timeout, keep_alive_timeout)
loop = asyncio.get_event_loop()
loop.run_forever()
except KeyboardInterrupt:
Expand Down
13 changes: 13 additions & 0 deletions robyn/robyn.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,19 @@ class Server:
) -> None:
pass
def start(self, socket: SocketHeld, workers: int, client_timeout: int, keep_alive_timeout: int) -> None:
"""
Start the Robyn server.

:param socket: The shared socket to listen on.
:param workers: Number of worker threads.
:param client_timeout: Maximum time in seconds to wait for the client
to transmit the complete request headers. This does **not** limit
handler execution time or overall request duration — it only guards
against slow or stalled clients during the initial header-reading
phase (maps to actix-web's ``client_request_timeout``).
:param keep_alive_timeout: Time in seconds to keep an idle connection
open before closing it.
"""
pass

class WebSocketConnector:
Expand Down
15 changes: 13 additions & 2 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,22 @@ impl Server {
}
}

/// Start the Robyn server.
///
/// * `client_timeout` – maximum seconds to wait for the client to transmit
/// the complete request headers. This does **not** limit handler execution
/// time or overall request duration; it only guards against slow or
/// stalled clients during the initial header-reading phase. Mapped to
/// actix-web's `client_request_timeout`.
/// * `keep_alive_timeout` – seconds to keep an idle connection open before
/// closing it. Mapped to actix-web's `KeepAlive::Timeout`.
pub fn start(
&mut self,
_py: Python,
socket: PyRef<SocketHeld>,
workers: usize,
client_timeout: u64,
keep_alive_timeout: u64,
) -> PyResult<()> {
pyo3_log::init();

Expand Down Expand Up @@ -275,9 +286,9 @@ impl Server {
},
))
})
.keep_alive(KeepAlive::Os)
.keep_alive(KeepAlive::Timeout(std::time::Duration::from_secs(keep_alive_timeout)))
.workers(workers)
.client_request_timeout(std::time::Duration::from_secs(0))
.client_request_timeout(std::time::Duration::from_secs(client_timeout))
Comment on lines +289 to +291
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In actix-web HttpServer, what exactly does client_request_timeout(Duration) control? Does it terminate a request when the handler itself runs longer than the timeout (e.g., handler sleeps 10 seconds)? Please cite official actix-web docs.

💡 Result:

In actix-web's HttpServer, client_request_timeout(Duration) controls the timeout for reading the client request head (headers). If the client does not transmit the entire set of headers within this time, the request is terminated with a 408 (Request Timeout) error. The default is 5000 milliseconds (5 seconds). To disable, set to Duration::ZERO. It does NOT terminate a request when the handler itself runs longer than the timeout (e.g., a handler that sleeps 10 seconds). This timeout only applies to initial header reading, not handler execution time.

Citations:


client_request_timeout does not enforce handler execution timeout (Line 282).

The manual test assumes that client_request_timeout will terminate requests when the handler runs longer than the timeout (e.g., handler sleeps 10 seconds). However, client_request_timeout only controls the timeout for reading client request headers, not handler execution time. A handler that sleeps 10 seconds will not be terminated by this setting. To enforce handler execution timeouts, a different mechanism is required (e.g., middleware-based timeout or tokio::time::timeout).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server.rs` around lines 280 - 282, The current use of
client_request_timeout only affects request header read time and does not stop
long-running handlers; update the server to enforce handler execution timeouts
by adding a middleware or wrapping handler execution with a timeout (e.g., add a
tower::timeout::TimeoutLayer or apply tokio::time::timeout around request
handling) instead of relying on client_request_timeout; locate where the server
is configured (the block using KeepAlive::Timeout, .workers,
.client_request_timeout) and wrap the service or router with a timeout layer (or
modify the request-handling entrypoint to use tokio::time::timeout) so that
handler functions are aborted when they exceed the desired duration.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolve — informational comment about Actix semantics, no code change needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@sansyrox have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 49 seconds before sending another message.

.listen(raw_socket.into())
.unwrap()
.run()
Expand Down
Loading