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
| Interface | Endpoint |
|---|---|
| WSS (Mainnet) | wss://polygon-mainnet.gateway.tatum.io |
Testnet WebSocket AvailabilityWebSocket 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.
- Install Node.js (version 22+):
brew install node
node -v- Create your project:
mkdir tatum-wss-test
cd tatum-wss-test
npm init -y
npm install -D tsx typescript @types/node- 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_subscribecall - 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, broadlogsfilters) 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
AttentionThis 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_unsubscribewhen a subscription is no longer needed.
1. newHeads block headers
newHeads block headersReceive 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:
| Field | Description | Example (decoded) |
|---|---|---|
number | Block height (hex) | 20,200,935 |
hash | Block hash | 0xabc123... |
timestamp | Unix timestamp (hex) | 1719795712 |
baseFeePerGas | EIP-1559 base fee (hex, in wei) | 1 Gwei = 0x3b9aca00 |
gasUsed | Total gas consumed (hex) | 15,000,000 |
gasLimit | Block gas limit (hex) | 30,000,000 |
miner | Fee recipient address | 0x... |
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
newPendingTransactions mempool activityReceive 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
newPendingTransactionsemits 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_getTransactionByHashlookups (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
logs smart contract eventsSubscribe 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:
| Parameter | Type | Description |
|---|---|---|
address | string 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 theremovedFieldWhen 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
| Symptom | Check |
|---|---|
| Connection drops immediately | Verify your API key is valid and included in the URL |
| No subscription ID returned | The subscribe call was malformed, check the JSON structure |
| No notifications received | Confirm your address/topics filter actually matches on-chain activity |
| Frequent disconnects | Add reconnect logic with exponential backoff and jitter |
| Overwhelming notification volume | Narrow your logs filter, or move off newPendingTransactions entirely if you don't need raw mempool data |
WebSocket vs. HTTP Polling
| Feature | WebSocket | HTTP Polling |
|---|---|---|
| Protocol | Persistent WS connection | Repeated HTTP requests |
| Latency | Real-time push (~0ms after node processes) | Poll interval dependent (1s to 15s typical) |
| Bandwidth | Efficient (events only) | Wasteful (many empty responses) |
| Complexity | Moderate (connection management) | Simple (stateless requests) |
| Mempool access | ✅ newPendingTransactions | ❌ Not available |
| Event streaming | ✅ logs subscription with filters | ❌ Must poll eth_getLogs |
| Best for | Dashboards, real-time UIs, event-driven backends | Simple 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