eth_subscribe: Stream Heads, Logs and Pending Transactions
eth_subscribe•Read method•Ethereum JSON-RPCeth_subscribe turns the request-response API into a stream. Over a WebSocket or IPC connection you name what you want to watch, the node returns a subscription id, and from then on it pushes notifications as events happen. It is how you watch the chain without polling, and the only practical way to see pending transactions as they arrive.
Try eth_subscribe
curl -s ws://eth:8545 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_subscribe",
"params": [
"newHeads"
]
}'Subscriptions need a persistent connection, so this one cannot be fired from the request builder: sent over HTTP, a node answers with the error "notifications not supported". Point a WebSocket client such as websocat or wscat at a ws:// endpoint and send the same JSON body, or use the ethers example below. Inside a BLAZED.sh container the endpoint is the local socket shown above.
What eth_subscribe does
There are four subscription types. newHeads pushes a block header for every block the node imports. logs takes an eth_getLogs-style filter and pushes matching logs as blocks are executed. newPendingTransactions pushes transactions as they enter the node's mempool, hashes by default or full transaction objects when the second parameter is true on Geth. syncing reports sync status transitions and is mostly an operational tool.
Notifications do not look like responses. The initial call returns a normal JSON-RPC result containing the subscription id, and everything after that arrives as a server-initiated message with method eth_subscription and a params object holding the subscription id and the payload. Client libraries hide this, but if you are writing against a raw WebSocket you have to route messages by shape rather than by request id.
The subscription lives on the connection. Close the socket, lose the network, or let a proxy time the connection out, and the subscription is gone with no notification and no replay of what you missed. Every production consumer therefore pairs the stream with a cursor: remember the last block you fully processed, and on reconnect backfill the gap with eth_getLogs before trusting the stream again.
Parameters
| # | Name | Type | Description |
|---|---|---|---|
| 1 | subscriptionName | string | One of newHeads, logs, newPendingTransactions or syncing. |
| 2 | optionsoptional | object | boolean | For logs, a filter object with address and topics using eth_getLogs matching rules. For newPendingTransactions on Geth, a boolean: true streams full transaction objects instead of hashes. |
What it returns
The immediate response is a hex subscription id. Afterwards the node pushes eth_subscription notifications carrying that id and a result whose shape depends on the type: a block header for newHeads, a log object for logs, a transaction hash (or full transaction) for newPendingTransactions.
eth_unsubscribe with the id stops the stream and returns true. Dropping the connection has the same effect without the courtesy of a confirmation.
Example response
// the call itself
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x9cef478923ff08bf67fde6c64013158d"
}
// then, one of these per imported block
{
"jsonrpc": "2.0",
"method": "eth_subscription",
"params": {
"subscription": "0x9cef478923ff08bf67fde6c64013158d",
"result": {
"number": "0x18789ac",
"hash": "0x7e9ea4877f25847c29ea338043948a8ea43383da1620ace955a20baf667c5ff5",
"parentHash": "0x161ab6d812fb274e0d07f71f197b458be7f96451932b129af3001487d7829a0c",
"timestamp": "0x6a6ddccb",
"gasUsed": "0x1e513f7",
"baseFeePerGas": "0x2e0a427"
}
}
}Header trimmed; the real notification carries the full header including the state, transaction and receipt roots. Note that the notification has no id field, because the node initiated it.
eth_subscribe with ethers.js
import { WebSocketProvider, id } from "ethers";
// inside a BLAZED.sh container the node is one local socket away
const provider = new WebSocketProvider("ws://eth:8545");
// eth_subscribe("newHeads")
provider.on("block", (blockNumber) => console.log("head", blockNumber));
// eth_subscribe("logs", { address, topics })
provider.on(
{
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
topics: [id("Transfer(address,address,uint256)")],
},
(log) => console.log("transfer in", log.blockNumber, log.transactionHash),
);
// eth_subscribe("newPendingTransactions"): the mempool as the node sees it
provider.on("pending", async (hash) => {
const tx = await provider.getTransaction(hash);
if (tx) console.log("pending", tx.from, "->", tx.to);
});Gotchas and common errors
HTTP cannot carry a subscription
Sending eth_subscribe to an https:// endpoint returns "notifications not supported" because there is no channel for the node to push down. You need ws:// or wss://, or a local IPC socket. If you are stuck on HTTP, eth_newFilter plus polling is the fallback that exists precisely for this case.
A dropped connection is silent data loss
Nothing tells you a subscription ended, and nothing replays what happened while you were away. Reconnect logic is mandatory: keep the last fully processed block number, re-subscribe on reconnect, and backfill the gap with eth_getLogs before resuming. Idle proxies and load balancers close quiet connections, so a heartbeat or a periodic call on the same socket is worth adding.
newPendingTransactions is a firehose
Mainnet's public mempool produces a continuous stream of transactions, and asking for full objects rather than hashes multiplies the bandwidth for each one. Shared providers commonly disable this subscription, sample it, or filter it, so what you see through a gateway is not what the node saw. A node your code runs on gives you its unfiltered view, which is the entire point for mempool-driven strategies.
Streams are per node, not global
Pending transactions arrive in the order this node learned about them, which differs from every other node's order and omits anything routed privately to builders. Two subscribers on two nodes see overlapping but different streams. Treat the feed as this node's view of the network rather than as a canonical sequence.
Reorgs come through the stream too
newHeads emits headers for blocks that later get reorged out, so you can legitimately see two different blocks at the same height. logs subscriptions deliver removed: true entries for the logs of orphaned blocks. Anything writing to a database off these streams needs to key on block hash and undo on removal.
Subscriptions leak if you never unsubscribe
Long-lived processes that re-subscribe on every reconnect, or create a subscription per user request, accumulate them on the node until the connection closes. Track the ids you hold and call eth_unsubscribe when a consumer goes away.
What eth_subscribe costs on BLAZED.sh
The eth_subscribe call itself costs 1 credit on BLAZED.sh, and WebSocket usage follows the same per-request credit model as HTTP. Compared with polling, the accounting is dramatically simpler: one subscription replaces a request every second forever. A newPendingTransactions stream is the one to watch, since the volume of pushed data on a busy chain is substantial. Running the consumer on the node host means that stream never crosses the public internet at all; it arrives over a local socket, which is where the sub-10ms round-trip on the follow-up calls comes from.
See the full credit price listeth_subscribe: frequently asked questions
Why does eth_subscribe return "notifications not supported"?
You sent it over HTTP. Subscriptions require a connection the node can push down, which means WebSocket or IPC. Over HTTP, use eth_newFilter with eth_getFilterChanges instead.
How do I watch the mempool?
Subscribe to newPendingTransactions. You get hashes by default, or full transaction objects by passing true on Geth. Bear in mind the pool is node-local and that privately routed transactions never appear in it.
What happens to my subscription if the connection drops?
It ends, silently, and nothing is replayed. Reconnect, re-subscribe, and backfill the gap using your last processed block number with eth_getLogs before trusting the live stream again.
Can I subscribe to logs for several contracts at once?
Yes. The logs filter takes the same address and topics fields as eth_getLogs, and address accepts an array. One subscription with several addresses is cheaper and simpler than one subscription per contract.
Is eth_subscribe billed per notification?
No. WebSocket subscriptions follow the same per-request credit model as any other call on BLAZED.sh; the subscription request is the billable event, and the large-response surcharge applies by the same rule as elsewhere.
Call eth_subscribe from the node itself
Deploy your container or script next to a synced Ethereum node. No rate limits, no compute units, sub-10ms local RPC.