Polygon - WebSocket

Learn how to use Tatum's Polygon WebSocket endpoint to subscribe to real-time on-chain events including new block headers, pending transactions, and smart contract logs.

Polygon's WebSocket interface (via eth_subscribe) pushes real-time notifications to your application over a persistent connection, with no polling required.

With Tatum's Polygon WebSocket endpoint, you can subscribe to:


Polygon WebSocket endpoint

InterfaceEndpoint
WSS (Mainnet)wss://polygon-mainnet.gateway.tatum.io
ℹ️

Testnet WebSocket Availability

WebSocket connections are currently available for Polygon Mainnet only. Testnet support HTTP RPC but do not expose WebSocket endpoints at this time. Use HTTP RPC for testnet development and WebSocket for mainnet production workloads.

For JSON-RPC (HTTP) and the full network list, see Polygon RPC.


Getting started

Environment setup

This guide uses Node.js 22+ with the native WebSocket API and TypeScript via tsx.

  1. Install Node.js (version 22+):
   brew install node
   node -v
  1. Create your project:
   mkdir tatum-wss-test
   cd tatum-wss-test
   npm init -y
   npm install -D tsx typescript @types/node
  1. Get your API key from the Tatum Dashboard.

Connection and authentication

The API key is passed as a path segment on the WebSocket URL.

const API_KEY = "YOUR_API_KEY";
const ENDPOINT = `wss://polygon-mainnet.gateway.tatum.io/${API_KEY}`;

const ws = new WebSocket(ENDPOINT);

Quick start checklist

Before opening subscriptions, confirm:

  • Your WebSocket connection opens without error
  • You receive a hex subscription ID in the server response after the first eth_subscribe call
  • Your service can hold long-lived outbound connections open
  • You have a reconnect strategy, WebSocket connections drop, assume it and rebuild on reconnect
  • You're prepared for high-volume subscriptions (newPendingTransactions, broad logs filters) with client-side throttling

End-to-end example

The following script connects to the endpoint and subscribes to new block headers. It is the fastest way to confirm your credentials and connection are working.

const API_KEY = "YOUR_API_KEY";
const ENDPOINT = `wss://polygon-mainnet.gateway.tatum.io/${API_KEY}`;

const ws = new WebSocket(ENDPOINT);

ws.onopen = () => {
  console.log("connected");

  ws.send(JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "eth_subscribe",
    params: ["newHeads"]
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data as string);

  if (msg.id === 1 && msg.result) {
    console.log("subscription id:", msg.result);
    return;
  }

  if (msg.method === "eth_subscription") {
    const block = msg.params.result;
    console.log("new block:", parseInt(block.number, 16));
    console.log("hash:", block.hash);
    console.log("gas used:", parseInt(block.gasUsed, 16));
  }
};

ws.onerror = (err) => console.error("error:", err);
ws.onclose = () => console.log("connection closed");

Run it with:

npx tsx index.ts
📘

Note

tsx runs TypeScript files directly with no tsconfig.json required.

🚧

Attention

This example streams live Polygon blocks and will keep running until you stop it. On macOS, press Ctrl+C in the terminal to stop the stream once you have confirmed it is working.


Example requests

Note: These are minimal examples. For production, implement reconnection with exponential backoff, track subscription IDs, and call eth_unsubscribe when a subscription is no longer needed.

1. newHeads block headers

Receive a notification every time a new block is added to the chain (~2 seconds on Polygon mainnet).

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 1,
  method: "eth_subscribe",
  params: ["newHeads"]
}));

Notification payload:

{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0x1a2b3c...",
    "result": {
      "number": "0x131a5e7",
      "hash": "0xabc123...",
      "parentHash": "0xdef456...",
      "timestamp": "0x66800000",
      "miner": "0x...",
      "gasUsed": "0x1234567",
      "gasLimit": "0x1c9c380",
      "baseFeePerGas": "0x3b9aca00",
      "transactionsRoot": "0x...",
      "stateRoot": "0x...",
      "receiptsRoot": "0x...",
      "logsBloom": "0x...",
      "extraData": "0x...",
      "difficulty": "0x0",
      "nonce": "0x0000000000000000"
    }
  }
}

Key fields:

FieldDescriptionExample (decoded)
numberBlock height (hex)20,200,935
hashBlock hash0xabc123...
timestampUnix timestamp (hex)1719795712
baseFeePerGasEIP-1559 base fee (hex, in wei)1 Gwei = 0x3b9aca00
gasUsedTotal gas consumed (hex)15,000,000
gasLimitBlock gas limit (hex)30,000,000
minerFee recipient address0x...

Best for: Block explorers, gas price dashboards, chain liveness monitoring.

Important: newHeads only delivers the block header. To get the full block with transactions, send a follow-up eth_getBlockByNumber call over the same WebSocket:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 100,
  method: "eth_getBlockByNumber",
  params: [block.number, true] // true = include full transaction objects
}));

To unsubscribe, call eth_unsubscribe with the subscription ID returned by the server:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 2,
  method: "eth_unsubscribe",
  params: [SUBSCRIPTION_ID]
}));

2. newPendingTransactions mempool activity

Receive a notification for each transaction hash that enters the node's mempool. This is a high-volume subscription, Polygon mainnet processes thousands of pending transactions per minute.

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 2,
  method: "eth_subscribe",
  params: ["newPendingTransactions"]
}));

Notification payload:

{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0x4d5e6f...",
    "result": "0x9876543210abcdef..."
  }
}

The result is a transaction hash only, not the full transaction object. To get transaction details, make a follow-up RPC call:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 200,
  method: "eth_getTransactionByHash",
  params: ["0x9876543210abcdef..."]
}));
⚠️

High Volume Warning

newPendingTransactions emits hundreds of hashes per second on Polygon mainnet. Always implement throttling on the client side, don't attempt to fetch details for every hash. A typical approach:

  • Cap concurrent eth_getTransactionByHash lookups (e.g., 3 to 5 at a time)
  • Skip hashes while at capacity
  • Keep only the N most recent pending transactions in your UI

Best for: Mempool dashboards, gas estimation, MEV monitoring, pending transaction previews.

To unsubscribe:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 3,
  method: "eth_unsubscribe",
  params: [SUBSCRIPTION_ID]
}));

3. logs smart contract events

Subscribe to filtered event logs emitted by smart contract transactions. This is the most flexible subscription type, supporting filtering by contract address and event topics.

// Subscribe to all ERC-20 Transfer events from USDT
ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 3,
  method: "eth_subscribe",
  params: [
    "logs",
    {
      address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", // USDT
      topics: [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" // Transfer(address,address,uint256)
      ]
    }
  ]
}));

Notification payload:

{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0x7a8b9c...",
    "result": {
      "address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
        "0x000000000000000000000000sender_address_here",
        "0x000000000000000000000000recipient_address_here"
      ],
      "data": "0x00000000000000000000000000000000000000000000000000000000003d0900",
      "blockNumber": "0x131a5e7",
      "transactionHash": "0x...",
      "transactionIndex": "0x0",
      "blockHash": "0x...",
      "logIndex": "0x0",
      "removed": false
    }
  }
}

Filter options:

ParameterTypeDescription
addressstring or string[]Contract address(es) to filter. Omit for all contracts.
topics(string | string[] | null)[]Up to 4 indexed topic filters. Supports OR within a position ([null, [topicA, topicB]]).

Common filter patterns:

// All events from a specific contract
{ address: "0xContractAddress" }

// Transfer events from any ERC-20 contract
{ topics: ["0xddf252ad..."] }

// Transfer events TO a specific address
{
  topics: [
    "0xddf252ad...",     // Transfer event signature
    null,                 // any sender
    "0x000...recipient"   // specific recipient (padded to 32 bytes)
  ]
}

// Events from multiple contracts
{ address: ["0xContract1", "0xContract2"] }
ℹ️

Understanding the removed Field

When a chain reorganization (reorg) occurs, previously emitted logs may become invalid. In that case, the same log is re-emitted with "removed": true. Your application should handle this by reverting any state changes derived from that log.

Best for: Token transfer feeds, DEX swap monitoring, NFT marketplace tracking, protocol-specific event streaming.

To unsubscribe:

ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 4,
  method: "eth_unsubscribe",
  params: [SUBSCRIPTION_ID]
}));

Troubleshooting

SymptomCheck
Connection drops immediatelyVerify your API key is valid and included in the URL
No subscription ID returnedThe subscribe call was malformed, check the JSON structure
No notifications receivedConfirm your address/topics filter actually matches on-chain activity
Frequent disconnectsAdd reconnect logic with exponential backoff and jitter
Overwhelming notification volumeNarrow your logs filter, or move off newPendingTransactions entirely if you don't need raw mempool data

WebSocket vs. HTTP Polling

FeatureWebSocketHTTP Polling
ProtocolPersistent WS connectionRepeated HTTP requests
LatencyReal-time push (~0ms after node processes)Poll interval dependent (1s to 15s typical)
BandwidthEfficient (events only)Wasteful (many empty responses)
ComplexityModerate (connection management)Simple (stateless requests)
Mempool accessnewPendingTransactions❌ Not available
Event streaminglogs subscription with filters❌ Must poll eth_getLogs
Best forDashboards, real-time UIs, event-driven backendsSimple scripts, serverless functions, one-off queries

Recommendation:

  • Use WebSocket for: real-time dashboards, block explorers, mempool monitoring, event-driven architectures
  • Use HTTP RPC for: serverless functions, one-off queries, simple scripts, environments where persistent connections aren't possible