# OpenEnv WebSocket Error Handling and Timeout Configuration: Complete Guide

> Master OpenEnv WebSocket error handling and timeout configuration. Learn to manage session idle duration and tool execution limits effectively with this complete guide.

- Repository: [Hugging Face/OpenEnv](https://github.com/huggingface/OpenEnv)
- Tags: how-to-guide
- Published: 2026-06-15

---

**OpenEnv uses a FastAPI WebSocket endpoint at `/ws` with structured error responses via `WSErrorResponse`, while separate timeout mechanisms control session idle duration through `ConcurrencyConfig.session_timeout` and tool execution limits via `MCP_TOOL_CALL_TIMEOUT`.**

OpenEnv, an open-source environment platform from Hugging Face, runs interactive sessions over persistent WebSocket connections. Understanding how the server handles malformed messages, unexpected exceptions, and timeout scenarios is essential for building robust client applications that interact with the environment server.

## How OpenEnv Handles WebSocket Errors

The WebSocket handler resides in **[`src/openenv/core/env_server/http_server.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_server/http_server.py)** and manages the full lifecycle of client connections. When a client connects to the `/ws` endpoint, the server establishes a dedicated session and enters a message processing loop.

### Connection Lifecycle and Message Dispatch

The connection flow follows three distinct phases:

1. **Connection setup** – Upon receiving a client connection, the server calls `await websocket.accept()` and initializes a dedicated environment instance through `_create_session`.
2. **Message loop** – The server continuously reads text frames, parses them as JSON, and dispatches commands based on the `"type"` field (`reset`, `step`, `state`, or `close`).
3. **Session cleanup** – When the client disconnects or timeouts occur, the server tears down the environment instance and releases resources.

### Structured Error Responses

All errors are wrapped in the **`WSErrorResponse`** model defined in **[`src/openenv/core/env_server/types.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_server/types.py)** to ensure clients never receive raw Python tracebacks:

```python
class WSErrorResponse(BaseModel):
    """WebSocket response for errors."""
    type: Literal["error"] = Field(default="error")
    data: Dict[str, Any] = Field(
        description="Error details including message and code"
    )

```

The server handles three specific error categories:

- **Invalid JSON** – Malformed payloads trigger a `WSErrorResponse` with `WSErrorCode.INVALID_JSON`.
- **Schema validation errors** – Pydantic validation failures are caught and returned as structured error responses.
- **Unexpected exceptions** – Any unhandled runtime exceptions are wrapped in the standard error format before transmission.

## Configuring WebSocket Timeouts in OpenEnv

OpenEnv implements two independent timeout mechanisms that operate at different granularity levels. Both are configurable through server-side settings or per-action parameters.

### Session Idle Timeout

The **session idle timeout** controls how long a WebSocket connection can remain inactive before automatic termination. This setting is defined in `ConcurrencyConfig.session_timeout` within **[`src/openenv/core/env_server/types.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_server/types.py)**.

The reaper loop runs inside `EnvServer._idle_session_reaper` (approximately lines 480-495 in [`http_server.py`](https://github.com/huggingface/OpenEnv/blob/main/http_server.py)). It monitors the `last_activity_at` timestamp of each `SessionInfo` object:

```python

# In http_server.py

self._session_idle_timeout_s = self._concurrency_config.session_timeout
...
if now - info.last_activity_at > timeout:
    # session idle too long → close it

```

Setting `session_timeout` to `None` disables automatic cleanup, though this is not recommended for production deployments.

### MCP Tool-Call Timeout

The **MCP tool-call timeout** restricts execution duration for individual tool invocations within an MCP-enabled environment. This default ceiling is defined by the constant `MCP_TOOL_CALL_TIMEOUT` in **[`src/openenv/core/env_server/mcp_environment.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_server/mcp_environment.py)**.

In `MCPEnvironment._handle_call_tool`, the server applies the timeout using this logic:

```python

# In mcp_environment.py

timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
...
except subprocess.TimeoutExpired:
    raise ToolError(
        f"Tool '{action.tool_name}' timed out after {timeout} seconds",
        code=WSErrorCode.TIMEOUT,
    )

```

The default value is **30 seconds**, though individual actions can override this by including a `timeout_s` field in the step payload.

### How Timeouts Interact

Understanding the distinction between these timeouts prevents confusion during debugging:

- **Idle timeout** terminates the entire WebSocket session and destroys the environment instance. Clients must establish a new connection to continue.
- **Tool-call timeout** aborts only the specific tool execution. The session remains alive, allowing clients to retry the action or send other messages without reconnecting.

## Practical Implementation Examples

### Client-Side WebSocket Interaction

This example demonstrates proper error handling and timeout override when interacting with the OpenEnv server:

```python
import json
import asyncio
import websockets

async def run():
    async with websockets.connect("ws://localhost:8000/ws") as ws:
        # 1️⃣ Reset the environment

        await ws.send(json.dumps({"type": "reset", "data": {}}))
        print(await ws.receive())               # → {"type":"observation",...}

        # 2️⃣ Step with a custom timeout (overrides default 30s)

        step_msg = {
            "type": "step",
            "data": {"action": {"code": "print('hi')"}, "timeout": 5}
        }
        await ws.send(json.dumps(step_msg))
        print(await ws.receive())               # ← may contain an error if >5s

        # 3️⃣ Query the state

        await ws.send(json.dumps({"type": "state"}))
        print(await ws.receive())

        # 4️⃣ Close the session cleanly

        await ws.send(json.dumps({"type": "close"}))

asyncio.run(run())

```

If the client remains silent longer than the configured `session_timeout`, the server closes the socket and subsequent `receive()` calls raise `websockets.exceptions.ConnectionClosed`.

### Server-Side Timeout Configuration

When launching the environment server, adjust timeouts through the `ConcurrencyConfig`:

```python
from openenv.core.env_server import ConcurrencyConfig, create_fastapi_app

config = ConcurrencyConfig(
    max_concurrent_envs=4,
    session_timeout=30.0        # 30s idle timeout

)
app = create_fastapi_app(
    env=my_env,
    action_cls=MyAction,
    observation_cls=MyObs,
    max_concurrent_envs=4,
    concurrency_config=config,
)

```

## Summary

- **Error Handling**: OpenEnv wraps all WebSocket errors in the `WSErrorResponse` model defined in [`types.py`](https://github.com/huggingface/OpenEnv/blob/main/types.py), ensuring consistent JSON error formats without exposing internal tracebacks.
- **Session Timeout**: Configure idle session duration via `ConcurrencyConfig.session_timeout` in [`http_server.py`](https://github.com/huggingface/OpenEnv/blob/main/http_server.py); the `_idle_session_reaper` task enforces this limit automatically.
- **Tool Timeout**: Control individual action execution through `MCP_TOOL_CALL_TIMEOUT` (default 30s) in [`mcp_environment.py`](https://github.com/huggingface/OpenEnv/blob/main/mcp_environment.py), overridable per-action via the `timeout` field.
- **Architecture**: The server separates transport-level session management from execution-level tool timeouts, allowing granular control over resource constraints.

## Frequently Asked Questions

### How does OpenEnv handle malformed JSON in WebSocket messages?

When the server receives invalid JSON, it catches the parsing exception and returns a `WSErrorResponse` with `WSErrorCode.INVALID_JSON`. This prevents the connection from closing unexpectedly and allows the client to correct the payload format without reconnecting.

### What happens when a tool execution exceeds the MCP timeout?

If a tool call exceeds the `MCP_TOOL_CALL_TIMEOUT` (or the per-action override), the server raises a `ToolError` with code `WSErrorCode.TIMEOUT`. The WebSocket session remains open, and the client receives a structured error response indicating which tool timed out and after how many seconds.

### Can I disable the session idle timeout entirely?

Yes, by setting `session_timeout` to `None` in the `ConcurrencyConfig` when calling `create_fastapi_app`. However, this is generally discouraged for production use as it allows orphaned sessions to consume resources indefinitely. The reaper loop in [`http_server.py`](https://github.com/huggingface/OpenEnv/blob/main/http_server.py) skips sessions without a timeout configured.

### Where is the WebSocket endpoint defined in the OpenEnv source code?

The `/ws` endpoint handler is implemented in **[`src/openenv/core/env_server/http_server.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_server/http_server.py)**, specifically within the `EnvServer` class. This file also contains the `_idle_session_reaper` method (lines ~480-495) that manages session lifecycle based on the `last_activity_at` timestamps tracked in `SessionInfo` objects.