# Understanding OpenEnv EnvClient WebSocket Communication Patterns and Message Timeouts

> Explore OpenEnv's EnvClient WebSocket communication patterns and timeouts. Learn how connect_timeout_s and message_timeout_s ensure low-latency, stateful interactions with typed JSON messages for efficient environment control.

- Repository: [Hugging Face/OpenEnv](https://github.com/huggingface/OpenEnv)
- Tags: internals
- Published: 2026-06-14

---

**OpenEnv's `EnvClient` establishes persistent WebSocket connections with configurable `connect_timeout_s` and `message_timeout_s` parameters to enable low-latency, stateful environment interactions through typed JSON message envelopes.**

OpenEnv by Hugging Face provides a robust framework for remote environment interactions via WebSocket connections. The `EnvClient` class in [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) serves as the primary async interface, replacing traditional HTTP request-response cycles with bidirectional communication. This article examines how the client manages connection timeouts, message exchange patterns, and error handling based on the actual source code implementation.

## Connection Establishment and Timeout Configuration

### URL Normalization and WebSocket Conversion

The constructor accepts both `http://` and `ws://` URLs, converting them to WebSocket format via `convert_to_ws_url` in [`src/openenv/core/utils.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/utils.py). This normalization ensures consistent connection handling regardless of the input scheme.

### Configuring connect_timeout_s and message_timeout_s

Two critical timeout parameters control network behavior:

- **`connect_timeout_s`** (default **10 seconds**): Defines the maximum wait time for the initial TCP handshake
- **`message_timeout_s`** (default **60 seconds**): Controls how long the client waits for server responses after sending requests

These values are stored as instance attributes `_connect_timeout` and `_message_timeout` and govern all subsequent network operations.

### Localhost Proxy Bypass

When connecting to `localhost` or `127.0.0.1`, the client automatically sets the `NO_PROXY` environment variable to bypass HTTP proxies. This logic resides in `EnvClient.connect()` (lines 80-94) and prevents routing issues during local development.

### WebSocket Initialization

The `max_message_size_mb` parameter (default **100 MB**) determines the maximum message size passed to the underlying `websockets` library. The async `ws_connect` call creates the `ClientConnection` object stored in `self._ws`.

## WebSocket Message Exchange Patterns

### JSON Envelope Structure

All communication follows a standardized envelope format:

```json
{
  "type": "<reset|step|state|close|...>",
  "data": { ... }
}

```

### Sending and Receiving with Timeout Protection

The `_send()` method serializes envelopes using `json.dumps` and invokes `self._ws.send`, while `_receive()` blocks on `self._ws.recv()` wrapped in `asyncio.wait_for` using the configured `message_timeout_s`. If the timeout expires, `asyncio.TimeoutError` propagates to caller code.

The `_send_and_receive()` method combines these operations and checks for server-side `"error"` responses, raising `RuntimeError` with the server's message when encountered (lines 44-57).

### High-Level API Methods

The public API translates method calls into WebSocket envelopes:

- **`reset(**kwargs)`**: Sends `{"type":"reset","data":kwargs}` and returns `StepResult`
- **`step(action, **kwargs)`**: Sends `{"type":"step","data":_step_payload(action)}` with environment-specific payload conversion
- **`state()`**: Sends `{"type":"state"}` and returns the custom `StateT` type
- **`close()`**: Sends `{"type":"close"}` and terminates the socket

All methods delegate to `_send_and_receive()`, ensuring consistent timeout behavior across operations.

## Error Handling and Timeout Management

### Handling asyncio.TimeoutError

When the server fails to respond within `message_timeout_s`, the client raises `asyncio.TimeoutError`. Users should catch this exception to implement retry logic or timeout adjustments:

```python
try:
    result = await env.step(action)
except asyncio.TimeoutError:
    print("Server took too long – consider increasing message_timeout_s")

```

### Server-Side Error Propagation

If the server returns an envelope with `"type": "error"`, the client raises `RuntimeError` containing the error message. Connection failures during `connect()` raise `ConnectionError` with the underlying exception attached (lines 101-104).

## Synchronous Usage Patterns

For blocking codebases, the `.sync()` method returns a `SyncEnvClient` that executes async operations in an event loop. The wrapper respects the same timeout configurations because it forwards calls to the underlying `EnvClient`.

```python
from openenv.core.sync_client import SyncEnvClient

client = EchoEnv(base_url="http://localhost:8000", message_timeout_s=15.0).sync()

```

Related implementations include `GenericEnvClient` in [`src/openenv/core/generic_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/generic_client.py) for concrete environment implementations and `MCPClient` in [`src/openenv/core/mcp_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/mcp_client.py) for production JSON-RPC protocol usage.

## Practical Implementation Examples

### Basic Async Usage with Custom Timeouts

```python
from envs.coding_env.client import CodingEnv

# Configure timeouts for heavy-weight environments

env = CodingEnv(
    base_url="http://localhost:8000",
    connect_timeout_s=20.0,
    message_timeout_s=120.0,
)

# Async context manager pattern

async with CodingEnv(base_url="ws://localhost:8000",
                    message_timeout_s=30.0) as env:
    try:
        obs = await env.reset(seed=42)
        while not obs.done:
            action = {"code": "print('tick')"}
            obs = await env.step(action)
    except asyncio.TimeoutError:
        print("A step timed out")

```

### Synchronous Client Usage

```python
from envs.echo_env.client import EchoEnv

client = EchoEnv(base_url="http://localhost:8000",
                 message_timeout_s=15.0).sync()

with client:
    try:
        result = client.reset()
        result = client.step({"text": "Hello world"})
        print("Observation:", result.observation)
    except RuntimeError as err:
        print("Server error:", err)

```

### Handling Server Errors and Large Messages

```python

# Server error handling

async with EchoEnv(base_url="ws://localhost:8000") as env:
    try:
        await env.step({"invalid": "payload"})
    except RuntimeError as exc:
        print("Caught server error:", exc)

# Large message configuration for high-resolution observations

env = EchoEnv(
    base_url="ws://localhost:8000",
    max_message_size_mb=500.0
)

```

## Summary

- OpenEnv's `EnvClient` uses persistent WebSocket connections via [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) to minimize latency in multi-step episodes
- **Configurable timeouts**: `connect_timeout_s` (default 10s) and `message_timeout_s` (default 60s) protect against stalled connections
- **Message envelopes**: All communication uses typed JSON with `type` and `data` fields
- **Automatic proxy bypass**: Localhost connections automatically bypass HTTP proxies via `NO_PROXY` environment variable
- **Synchronous wrapper**: `SyncEnvClient` in [`src/openenv/core/sync_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/sync_client.py) provides blocking API access with identical timeout behavior
- **Error handling**: Distinguishes between `asyncio.TimeoutError` (network timeout) and `RuntimeError` (server-reported error)

## Frequently Asked Questions

### How does EnvClient handle WebSocket URL conversion?

The constructor automatically converts HTTP URLs to WebSocket format using `convert_to_ws_url` in [`src/openenv/core/utils.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/utils.py). This ensures that both `http://` and `ws://` schemes work correctly without manual intervention.

### What is the default message timeout in OpenEnv EnvClient?

The default `message_timeout_s` is **60 seconds**, while `connect_timeout_s` defaults to **10 seconds**. These values are stored in `_message_timeout` and `_connect_timeout` instance attributes and can be adjusted during client instantiation.

### How can I distinguish between network timeouts and server errors?

`asyncio.TimeoutError` indicates the server failed to respond within `message_timeout_s`, while `RuntimeError` indicates the server responded with an error message. The latter occurs when the server returns a JSON envelope with `"type": "error"`, as handled in `_send_and_receive()` (lines 44-57).

### Does the synchronous wrapper support the same timeout configurations?

Yes. `SyncEnvClient` respects the same `connect_timeout_s` and `message_timeout_s` values because it forwards method calls to the underlying `EnvClient` and runs them in an async event loop. Configure timeouts when creating the async client before calling `.sync()`.