You stream pending transactions by opening a WebSocket subscription to newPendingTransactions, then resolving each hash the socket pushes into a full transaction object. You filter that stream in one of two places: in your own code after decoding each transaction, or on the provider’s side with a subscription that only sends you matches.
The default stream is a firehose. On a busy chain, thousands of unconfirmed transactions flow through every minute, and almost none of them matter to your app. The skill worth learning is not just opening the socket — most tutorials stop there — but cutting the stream down to the handful of transactions you actually care about. This guide walks through the subscription, the code to consume it in a current library, and three ways to filter it.
What Is an Unconfirmed Transaction on Ethereum?

An unconfirmed transaction is one that has been broadcast to the network but not yet included in a block. It sits in the mempool — the memory pool each node keeps of transactions it has seen and validated but not yet mined.
Two details shape everything that follows. First, the mempool is node-local: each node holds its own view, so what one endpoint streams can differ slightly from another at the same instant. Second, a transaction in this state is temporary. It gets mined, replaced by a higher-fee transaction with the same nonce, or dropped when the pool fills up. For the mechanics of the pool itself — the txpool methods, private relays, why public endpoints hide most of it — see how to access the Ethereum mempool. This guide stays on the streaming side.
Why Subscribe Instead of Polling the Chain?
Because polling for new transactions is slow, wasteful, or both. Polling means your app repeatedly asks an endpoint “anything new?” — and the mempool changes far too fast for that to keep up without burning requests.
The pace makes the point. On Ethereum mainnet, new pending-transaction events land roughly every 700–800 milliseconds, according to Infura’s WebSocket documentation. A polling loop either checks that often and spends a request on each cycle, most returning nothing new, or checks less often and misses transactions between cycles. A WebSocket subscription inverts the model: you open one connection, and the node pushes each transaction the moment it arrives. No wasted calls, no fixed lag.
Who Watches the Mempool Stream, and for What?
The groups that consume this feed want very different slices of it, which is exactly why filtering matters.
- Trading and liquidation bots watch for calls to a specific DEX router or lending contract, ignoring everything else.
- Wallets and dApps track one user’s address to show an incoming payment before it confirms.
- Monitoring backends alert on activity against a protocol’s own contracts — an unusual sender, a known-risky method — in the seconds before it lands.
- Analytics tools sample the whole stream to measure gas pressure and fee trends.
Only the last group wants the raw firehose. The rest want a filter.
How to Open the Subscription
The subscription itself is one JSON-RPC message. You send eth_subscribe with newPendingTransactions as the parameter, over a WebSocket (wss://) connection rather than HTTP.
json
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newPendingTransactions"]}
By default this streams transaction hashes — a 32-byte identifier, not the transaction itself. To do anything useful with a match, you follow up with eth_getTransactionByHash to pull the full object. The ethereum.org WebSocket tutorial documents this subscribe-then-resolve pattern across the standard feeds.
One client-level shortcut is worth knowing. Geth accepts an optional second boolean on this subscription: pass true and it streams full transaction bodies instead of hashes, skipping the follow-up call, per the Geth pub/sub documentation.
json
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newPendingTransactions", true]}
Support for that second argument depends on the client and the endpoint exposing it, so test it against your specific provider before relying on it. The hashes-only form is the safe baseline.
Streaming It With a Library: ethers v6 and viem
In practice you rarely hand-roll the socket. A library manages the connection, the subscription, and the JSON framing for you. Two libraries dominate current Ethereum development, and both have moved on from the examples you’ll find in older guides.
A note on tooling before the code: if you’re reaching for web3.js, reconsider. ChainSafe, its maintainer, has announced that web3.js is being sunset and is steering developers elsewhere. New work belongs on ethers or viem.
ethers.js (v6)
The current major version is ethers v6, and its syntax differs from the v5 code in most tutorials — the provider is imported differently and values come back as native BigInt. The event name is still "pending", and it hands you a hash.
js
import { WebSocketProvider } from "ethers";
const provider = new WebSocketProvider("wss://your-endpoint");
provider.on("pending", async (txHash) => {
const tx = await provider.getTransaction(txHash);
if (!tx) return; // mined or dropped before we could fetch it
console.log(tx.hash, tx.to, tx.value.toString());
});
The if (!tx) return line is not optional in production. A hash can vanish from the pool between the moment you receive it and the moment you query it, and getTransaction then returns null.
viem
viem exposes the same feed through watchPendingTransactions, which calls your handler with an array of hashes. On a WebSocket transport it uses a real subscription; on an HTTP transport it falls back to polling, as the viem documentation describes.
ts
import { createPublicClient, webSocket } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: webSocket("wss://your-endpoint"),
});
const unwatch = client.watchPendingTransactions({
onTransactions: async (hashes) => {
for (const hash of hashes) {
const tx = await client.getTransaction({ hash });
console.log(tx.hash, tx.to, tx.value);
}
},
});
watchPendingTransactions returns an unwatch function — call it to close the subscription cleanly when you’re done.
Both examples point at a wss:// URL. That endpoint has to keep a socket open for you around the clock, which is the part that’s awkward to self-host just to watch a stream. The NOWNodes WebSocket API provides this persistent connection on supported networks including Ethereum, so your app can subscribe to pending activity without running an always-on client of its own. The wss:// address comes from a standard Ethereum endpoint.
How to Filter the Stream Down to What You Care About
This is where a working script becomes a useful one. You have two places to filter, and the right choice depends on how much of the stream you can afford to receive.
Filtering in Your Own Code
The straightforward approach: receive every hash, resolve it, and keep only the transactions that match your rule. Once you hold the full object, you can filter on any field. The most useful four:
| Filter on | Field | Example target |
|---|---|---|
| Who sent it | from | A wallet you monitor |
| Where it’s going | to | A DEX router or your own contract |
| How much ETH | value | Transfers above a threshold |
| What it calls | first 4 bytes of input | A specific function |
That last row is the sharp one. The first four bytes of a transaction’s input data are the function selector — a hash of the function signature that identifies which contract method is being called. Matching on it lets you watch for one specific action, like a swap, and ignore every other call to the same contract.
js
const UNISWAP_ROUTER = "0x7a250d5630b4cf539739df2c5dacb4c659f2488d";
provider.on("pending", async (txHash) => {
const tx = await provider.getTransaction(txHash);
if (!tx || tx.to?.toLowerCase() !== UNISWAP_ROUTER) return;
const selector = tx.data.slice(0, 10); // "0x" + 4 bytes
console.log("Router call:", tx.hash, "method:", selector);
});
This runs on any standard endpoint and gives you unlimited filtering logic. The cost is bandwidth: you still receive and resolve every transaction on the chain, then discard most of them.
Filtering on the Provider’s Side

Some providers let you push the filter upstream, so you only receive matches in the first place. Alchemy’s enhanced alchemy_pendingTransactions subscription accepts fromAddress, toAddress, and a hashesOnly flag, and when hashesOnly is false it streams the full transaction object directly — no follow-up call — according to Alchemy’s subscription documentation.
json
{"jsonrpc":"2.0","id":2,"method":"eth_subscribe",
"params":["alchemy_pendingTransactions",
{"toAddress":["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
"hashesOnly":false}]}
That example streams only pending transactions headed to the USDC contract, with full objects. For a high-volume target, receiving one in a thousand transactions instead of all of them is a real saving.
The trade-off is portability. newPendingTransactions is a standard method that behaves the same across providers; alchemy_pendingTransactions is specific to one. Auston Bunsen, co-founder of QuickNode, framed the general risk in an interview with Sacra: “I can go from Alchemy to Infura to QuickNode relatively quickly, unless I’m using one of their sort of custom APIs.” A filtered feed built on a vendor-specific method is exactly that kind of custom API — convenient now, a rewrite later if you switch.
| Client-side filter | Server-side filter | |
|---|---|---|
| Where it runs | Your code, after decoding | The provider, before sending |
| Bandwidth | You receive every transaction | You receive only matches |
| Filter logic | Anything you can write | Limited to supported params |
| Portability | Works on any endpoint | Tied to one provider’s method |
A common middle path: filter server-side on to/from to cut the volume, then apply the finer logic — value thresholds, selector matching — in your own code.
What Happens When the Socket Drops?
It will drop, so plan for it. A provider-side deploy, an idle timeout, or a network blip closes the connection eventually, and an app that assumes the socket stays open forever will silently stop receiving transactions.
The reconnection pattern is standard: detect the close, open a new connection, and re-subscribe. Pending data carries one wrinkle that block data does not. When you reconnect after missing blocks, you can backfill them over HTTP. You cannot backfill a mempool — the transactions you missed were never stored anywhere queryable, and many have already been mined or dropped. For a pending stream, reconnecting means accepting the gap and resuming, then reconciling against confirmed blocks if your logic needs certainty about what actually landed.
Conclusion
Opening the subscription is the easy 10% of this job. The part that decides whether your stream is usable is the filter: standard newPendingTransactions plus client-side logic when you want portability and full control, or a provider’s filtered subscription when the raw volume is too high to receive. Build the subscribe-resolve-filter loop, handle the null transactions, and wire in reconnection from the first commit.
Underneath all of it sits the same requirement — a low-latency WebSocket endpoint that stays open so you don’t miss what you’re watching for. When a standard subscription is no longer enough and you need higher-throughput, server-side-filtered delivery, gRPC streaming covers that next step on supported chains. Match the tool to how narrow your target is, and let the provider hold the socket open while you focus on the logic.
FAQ
Can I avoid the extra getTransactionByHash call for every hash?
Yes, in two ways. On Geth, pass true as the second argument to newPendingTransactions to stream full bodies. With an enhanced feed such as alchemy_pendingTransactions, set hashesOnly to false. Both skip the follow-up call, at the cost of a heavier stream and, for the enhanced feed, a provider-specific method.
What happens to a pending transaction I’m streaming if it’s dropped or replaced?
Nothing is pushed to tell you. The standard subscription emits a hash when a transaction enters the pool, but there is no “removed” event when one is dropped, replaced, or mined. If you need to know an outcome, reconcile the hashes you’ve seen against newly confirmed blocks rather than waiting for a signal that never comes.
Can I stream pending transactions on layer-2s and non-Ethereum chains?
It varies. Many EVM chains expose newPendingTransactions, but some layer-2 networks sequence transactions in a way that offers no public pending feed at all, so there is nothing to subscribe to. Non-EVM chains use entirely different interfaces. Always confirm that your specific network supports the subscription before building against it.
Should I filter in my own code or on the provider’s side?
Filter client-side when you want portability across providers and arbitrary logic, and can afford to receive the full stream. Filter server-side when the volume is high and your target is a known set of addresses — you receive far less data, but you’re tied to that provider’s method. Combining the two, coarse server-side then fine client-side, is often the most practical setup.



