> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-bridge-release-v6-7-0.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket Connections

> Connecting to Sei via WebSocket for real-time block and event subscriptions

# WebSocket connections

Sei supports `eth_subscribe` over WebSocket. With the WebSocket transports in standard libraries, you can subscribe to new blocks, event logs, and pending transactions.

## Endpoints

| Network | WebSocket endpoint |
| - | - |
| Sei Mainnet | `wss://evm-ws.sei-apis.com` |
| Sei Testnet | `wss://evm-ws-testnet.sei-apis.com` |

## Connecting

<CodeGroup>
  ```ts viem theme={"dark"}
  import { createPublicClient, webSocket } from 'viem';
  import { sei } from 'viem/chains';

  const client = createPublicClient({
    chain: sei,
    transport: webSocket('wss://evm-ws.sei-apis.com'),
  });
  ```

  ```ts ethers theme={"dark"}
  import { ethers } from 'ethers';

  const provider = new ethers.WebSocketProvider('wss://evm-ws.sei-apis.com');
  ```
</CodeGroup>

## Watching new blocks

<CodeGroup>
  ```ts viem theme={"dark"}
  const unwatch = client.watchBlocks({
    onBlock: (block) => {
      console.log('New block:', block.number);
    },
  });

  // Stop watching
  unwatch();
  ```

  ```ts ethers theme={"dark"}
  provider.on('block', (blockNumber) => {
    console.log('New block:', blockNumber);
  });

  // Stop watching
  provider.off('block');
  ```
</CodeGroup>

## Watching contract events

<CodeGroup>
  ```ts viem theme={"dark"}
  import { parseAbiItem } from 'viem';

  const unwatch = client.watchEvent({
    address: '0xContractAddress',
    event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
    onLogs: (logs) => {
      console.log('Transfer events:', logs);
    },
  });
  ```

  ```ts ethers theme={"dark"}
  import { ethers } from 'ethers';

  const ERC20_ABI = ['event Transfer(address indexed from, address indexed to, uint256 value)'];
  const contract = new ethers.Contract('0xContractAddress', ERC20_ABI, provider);

  contract.on('Transfer', (from, to, value, event) => {
    console.log('Transfer:', { from, to, value });
  });

  // Stop watching
  contract.off('Transfer');
  ```
</CodeGroup>

## Watching ERC-20 transfers across all contracts

```ts viem theme={"dark"}
import { parseAbiItem } from 'viem';

const unwatch = client.watchEvent({
  event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log(`Transfer on ${log.address}:`, log.args);
    });
  },
});
```

## Notes

* Sei has instant finality, so every block emitted over WebSocket is already final. You do not need to wait for more confirmations before you act on an event.
* Pending transaction subscriptions (`newPendingTransactions`) are supported at the RPC level, but Sei does not guarantee Ethereum-style pending state visibility.

## Frame size and concurrency limits

The WebSocket plane shares its admission-control settings with the HTTP JSON-RPC plane through the `[evm]` section of `app.toml`. Two limits apply to every WebSocket connection:

### Frame size (`max_request_body_bytes`)

Each inbound WebSocket frame is capped by `[evm].max_request_body_bytes`, the same knob that bounds HTTP request bodies. The default is 5 MiB (`5242880`). Frames larger than this limit cause the connection to close with WebSocket close code `1009` (message too big); no JSON-RPC error response is returned.

<Warning>
  Earlier node releases used a hardcoded 10 MiB frame cap on the WebSocket plane. Both planes now share `max_request_body_bytes` with a 5 MiB default. Operators whose WebSocket clients send frames in the 5–10 MiB range (for example large `eth_sendRawTransaction` batches or wide filter payloads) should set `max_request_body_bytes = 10485760` in `app.toml` before upgrading. Note that this also raises the HTTP body limit to 10 MiB.
</Warning>

### Concurrent request budget (`max_concurrent_request_bytes` and `ws_admission_timeout`)

`[evm].max_concurrent_request_bytes` bounds the total size of JSON-RPC request bytes admitted for processing concurrently. The WebSocket plane and the HTTP plane each get their own independent budget, so peak in-flight request bytes process-wide can reach twice this value.

When a WebSocket connection cannot be admitted immediately because the budget is exhausted, it blocks and waits for budget to free up. `[evm].ws_admission_timeout` bounds how long it waits (default `30s`; zero or negative values use the go-ethereum default of 30s). If the wait expires, the peer receives JSON-RPC error `-32005` ("timed out waiting for concurrent request-byte budget") and the connection is closed, dropping any active subscriptions along with it.

## `newHeads` under Autobahn consensus

When a node runs under Autobahn consensus, `eth_subscribe("newHeads")` notifications come from an in-process notifier, not from the legacy consensus event bus. The notifier publishes committed-block headers directly. Subscribers still observe headers only for fully committed blocks. However, the header payload differs from the legacy path in a few ways:

* **`parentHash`, `receiptsRoot`, and `transactionsRoot` are returned as zero hashes** (`0x0000…0000`). The Autobahn block-execution path does not build a Tendermint-style hash chain, so there is no meaningful value to return in these fields.
* **`stateRoot`** comes from the finalized block's `AppHash` (the post-execution application hash), not from a pre-execution header field.
* **`hash`** is the Autobahn block-header hash. It is the same value that `eth_getBlockByNumber` and the receipt APIs report as `blockHash`. This keeps `newHeads` consistent with the rest of the EVM RPC surface.
* **`gasUsed`** is an approximation (summed from per-transaction results) that keeps the notification cheap.

Because of these differences:

* Subscribers that chain-validate the head stream by linking `parentHash` values cannot rely on `newHeads` under Autobahn. They need a different mechanism.
* If you need the exact `gasUsed` or the omitted hash fields, fetch the block explicitly with `eth_getBlockByNumber`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.