Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Reliability & Consistency14 min read

Monitoring Pending Ethereum Transactions with eth_newPendingTransactionFilter

A practical deep-dive into eth_newPendingTransactionFilter: how the pending-pool filter works, why it is fragile, and how to build a resilient monitor.

TL;DR

eth_newPendingTransactionFilter creates a server-side filter that returns the hashes of transactions entering a node's pending pool. Unlike eth_newFilter, which returns logs, and eth_newBlockFilter, which returns block hashes, this filter returns only transaction hashes, so consumers must follow up with eth_getTransactionByHash or eth_getTransactionReceipt to learn anything about the transaction. The filter is polled with eth_getFilterChanges, which is destructive and advances an internal cursor, and filter ids are node-local state that expire after a short idle period. The critical caveat is that the method depends entirely on the node's txpool policy: providers that disable or prune the public pending pool will return nothing or a skewed subset, so any monitor must be validated against its own endpoint. This article explains the mechanism, provides runnable Node.js examples, and shows how to measure pending-hash throughput and resolvability per endpoint.

What eth_newPendingTransactionFilter Actually Does

eth_newPendingTransactionFilter creates a server-side filter on the node that returns the hashes of transactions entering the node's pending pool. It is one of three filter-creation methods in the Ethereum JSON-RPC specification, alongside eth_newFilter (which returns logs matching a topic/address filter) and eth_newBlockFilter (which returns new block hashes). The method takes no parameters and returns a filter id as a hexadecimal string.

The return value is a filter id, not a stream. The node maintains an internal queue of pending transaction hashes and exposes them only when the client calls eth_getFilterChanges with that id. This is a pull model: the client controls the polling cadence, and the node accumulates hashes between polls. The Ethereum JSON-RPC specification documents the method and its companion polling methods at eth_newPendingTransactionFilter.

Because the filter returns only hashes, it is a discovery mechanism, not a data source. To learn the sender, nonce, gas price, maxFeePerGas, or recipient of a pending transaction, the consumer must issue a follow-up eth_getTransactionByHash for each hash. That second call is where most of the latency and cost of a pending-transaction monitor actually lives.

  • eth_newPendingTransactionFilter: returns pending transaction hashes (no parameters).
  • eth_newFilter: returns logs matching address/topics over a block range.
  • eth_newBlockFilter: returns new block hashes.
  • All three return a filter id that must be polled with eth_getFilterChanges.

Cursor Semantics of eth_getFilterChanges and eth_getFilterLogs

eth_getFilterChanges is destructive. Each call returns only the items accumulated since the previous poll and advances an internal cursor, so a client that polls twice in quick succession will see the second call return an empty array if no new hashes arrived in between. This is documented behavior in the Ethereum JSON-RPC specification at eth_getFilterChanges. The cursor is per filter id and per node; it is not shared across clients or connections.

eth_getFilterLogs is not valid for a pending-transaction filter. That method returns the full set of logs matching a log filter and is intended for eth_newFilter. Calling it against a pending-transaction filter id is undefined or returns an error depending on the client. For pending transactions, eth_getFilterChanges is the only polling method that applies.

The destructive cursor has a practical consequence: if your poller crashes or restarts, the hashes accumulated since the last successful poll are lost unless the node still holds them in the filter queue. A robust monitor should treat each poll as a checkpoint and persist or forward the hashes it receives before doing anything else.

  • eth_getFilterChanges returns only new items since the last poll and advances the cursor.
  • eth_getFilterLogs is for log filters, not pending-transaction filters.
  • A crashed poller loses the hashes that arrived between the last poll and the crash.

Filter Lifecycle, Expiry, and Recreation

Filter ids are node-local state. They are not portable across nodes, across restarts, or across load-balanced connections that land on different backends. A filter created on one node cannot be polled on another, and a filter created before a node restart will not survive it. This is consistent with the general filter lifecycle described in the Ethereum eth_newFilter and getFilterChanges lifecycle article.

Idle filters are pruned after a short period. The exact timeout is client-dependent and is not fixed by the specification; common clients prune filters that have not been polled for a few minutes. A long-running monitor must therefore poll frequently enough to keep the filter alive, and it must be prepared to recreate the filter when a poll returns an unknown-filter-id error.

The recreation path is straightforward: catch the error, call eth_newPendingTransactionFilter again, and resume polling with the new id. The gap between the failed poll and the new filter creation is a window in which pending hashes may be missed. There is no way to recover those hashes from the node after the fact; the only mitigation is to keep the poll interval short relative to the expiry timeout.

  • Filter ids are node-local and do not survive restarts or node switches.
  • Idle filters are pruned after a client-dependent timeout.
  • On an unknown-id error, recreate the filter and accept a small gap.

The txpool Dependency: Why Results Vary by Provider

The pending-transaction filter reads from the node's transaction pool (txpool). If the node does not maintain a public pending pool, or if it prunes or blocks transactions by fee or by type, the filter will return nothing or a skewed subset. This is the single most important caveat for anyone building a pending-transaction monitor. The behavior is documented per client, but in practice it varies by provider and must be treated as 'documented / varies by provider'.

Some providers disable the public pending pool entirely to reduce memory pressure and to avoid exposing unconfirmed transaction data. Others keep a bounded pool and evict low-fee transactions first. A monitor that works against one endpoint may return zero hashes against another. The Ethereum transaction pool and txpool namespace article covers the txpool namespace and its inspection methods, which are useful for validating what a given node actually holds.

The practical implication is that a pending-transaction monitor must be validated against its own endpoint before it is trusted. Do not assume that a filter returning hashes on a local dev node will behave the same way on a hosted RPC endpoint. Measure first, then decide whether the endpoint is suitable for the use case.

  • The filter reads from the node's txpool; no pool means no hashes.
  • Fee-based or type-based pruning produces a skewed subset.
  • Validate the endpoint before relying on the monitor in production.

Polling with eth_newPendingTransactionFilter: A Runnable Node.js Example

The example below uses raw JSON-RPC over fetch. It creates a pending-transaction filter, polls eth_getFilterChanges on an interval, resolves each hash with eth_getTransactionByHash, and prints the sender, nonce, gasPrice, and maxFeePerGas. It also recreates the filter when the node returns an unknown-filter-id error.

Replace the RPC_URL with your endpoint. The poll interval is set to 2 seconds, which is short enough to keep most filters alive but still leaves a window in which bursts can be missed. Adjust it based on the expiry behavior you observe on your endpoint.

const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
let filterId = null;

async function rpc(method, params = []) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(json.error.message);
  return json.result;
}

async function createFilter() {
  filterId = await rpc('eth_newPendingTransactionFilter');
  console.log('created filter', filterId);
}

async function poll() {
  if (!filterId) await createFilter();
  let hashes;
  try {
    hashes = await rpc('eth_getFilterChanges', [filterId]);
  } catch (err) {
    console.warn('filter lost, recreating:', err.message);
    filterId = null;
    return;
  }
  for (const hash of hashes) {
    try {
      const tx = await rpc('eth_getTransactionByHash', [hash]);
      if (!tx) continue;
      console.log({
        hash,
        from: tx.from,
        nonce: tx.nonce,
        gasPrice: tx.gasPrice,
        maxFeePerGas: tx.maxFeePerGas
      });
    } catch (err) {
      console.warn('resolve failed', hash, err.message);
    }
  }
}

(async () => {
  await createFilter();
  setInterval(poll, 2000);
})();

Resolving Pending Hashes and Detecting Stuck Transactions

Once you have a pending hash, eth_getTransactionByHash returns the transaction object if the node still has it. If the transaction has been mined, the same call may return the mined transaction or null depending on the node's retention policy. For confirmation, use eth_getTransactionReceipt, which returns null while the transaction is pending. The eth_getTransactionReceipt null and pending receipt polling article covers the receipt-null case in detail.

A stuck transaction can be detected by correlating a pending hash with a later nonce gap in the txpool. If you track the highest nonce seen for a sender and then observe that a lower nonce never appears in subsequent polls, the transaction at that nonce is likely stuck. The txpool namespace (txpool_content, txpool_inspect) can confirm this on nodes that expose it, but many hosted endpoints do not.

A practical monitor keeps a per-sender map of seen nonces and flags a gap when a higher nonce is observed without the lower one. This is heuristic, not authoritative: the lower-nonce transaction may simply have been evicted from the pool, or the node may not have seen it at all. Treat the signal as a prompt to investigate, not as proof.

  • eth_getTransactionByHash resolves a pending hash to a transaction object.
  • eth_getTransactionReceipt returns null while the transaction is pending.
  • A nonce gap in the txpool is a heuristic signal of a stuck transaction.

Polling versus eth_subscribe('newPendingTransactions')

The WebSocket subscription eth_subscribe('newPendingTransactions') pushes pending hashes to the client as they arrive, rather than requiring the client to poll. This eliminates the poll interval as a source of missed hashes and removes the filter-expiry problem, because the subscription is tied to the connection rather than to a pruned filter id. The tradeoff is that the client must maintain a persistent WebSocket connection and handle reconnection logic.

The polling approach is simpler to operate over HTTP, works through proxies and load balancers that do not support long-lived connections, and is easier to reason about for batch processing. The subscription approach is better for low-latency monitoring and for workloads that can tolerate a persistent connection. The Ethereum eth_subscribe logs versus WebSocket polling article compares the two models for logs; the same tradeoffs apply to pending transactions.

Neither approach changes the underlying txpool dependency. A subscription to newPendingTransactions on a node with no public pending pool will also return nothing. The choice between polling and subscribing is about transport and latency, not about pool completeness.

  • eth_subscribe pushes hashes; eth_newPendingTransactionFilter requires polling.
  • Subscriptions avoid filter expiry but require a persistent connection.
  • Both depend on the node's txpool policy for what is actually visible.

A Reproducible Results Table for Your Endpoint

Because pending-pool behavior varies by provider, the only reliable way to know what an endpoint will do is to measure it. The table below is a template: fill it in against your own endpoint, using the same script and the same observation window. Do not compare numbers across endpoints unless the window and the network conditions are comparable.

Run the monitor for at least ten minutes during a period of normal network activity. Record the number of pending hashes returned per minute, the share of those hashes that resolve to a transaction object via eth_getTransactionByHash, whether the first poll after creation returns anything, and how long the filter survives without polling. The last row is measured by creating a filter and deliberately not polling it until an error occurs.

  • Pending hashes per minute: count eth_getFilterChanges results over a fixed window.
  • Share resolvable: fraction of hashes that return a non-null eth_getTransactionByHash.
  • First-poll-after-creation behavior: does the first poll return hashes or an empty array?
  • Filter expiry interval: time from creation to unknown-filter-id error with no polling.
  • Pool completeness: compare against a second endpoint or a local node if available.

Troubleshooting Common Failure Modes

The most common failure is an empty result set. If eth_getFilterChanges consistently returns an empty array, the first thing to check is whether the endpoint maintains a public pending pool. Try eth_newPendingTransactionFilter on a second endpoint and compare. If both return nothing, the network may simply be quiet, or both endpoints may disable the pool.

The second common failure is an unknown-filter-id error. This means the filter was pruned or the node restarted. The fix is to recreate the filter and resume polling. If this happens frequently, shorten the poll interval or switch to a WebSocket subscription. If it happens immediately after creation, the endpoint may not support the method at all.

The third failure is a high rate of unresolvable hashes. This happens when the node evicts transactions from the pool faster than your poller can resolve them, or when the pool is bounded and low-fee transactions are dropped. Reducing the poll interval and resolving hashes in parallel can help, but the underlying cause is pool policy. The Ethereum RPC node guide (RPC Assistant) covers endpoint selection and capability checks that can help you choose a suitable provider.

  • Empty results: check whether the endpoint has a public pending pool.
  • Unknown filter id: recreate the filter and shorten the poll interval.
  • Unresolvable hashes: pool eviction or bounded pool; reduce poll interval.

Limitations and Tradeoffs of Pending-Transaction Monitoring

The method does not stream. Hashes accumulate between polls, and a burst of transactions that arrives and is mined within a single poll interval may never be observed. This is inherent to the pull model and cannot be fully eliminated by shortening the interval, because the node's filter queue is bounded and the cursor is destructive.

Filter ids are not portable. They cannot be shared across nodes, across restarts, or across connections that may land on different backends. A monitor that assumes a stable filter id across a load-balanced pool will fail intermittently. The safe pattern is to treat the filter as ephemeral and recreate it on any error.

The public pending pool is not a reliable representation of global pending state. It reflects what a single node has seen and chosen to keep. Different nodes see different subsets, and providers apply different policies. For any application that needs a complete or authoritative view of pending transactions, the pending pool is the wrong source. Use it for discovery and heuristics, not for accounting.

  • No streaming: bursts can be missed between polls.
  • Filter ids are node-local and not portable.
  • The public pending pool is a partial, policy-dependent view.

Next Steps: Choosing an Endpoint and Building a Resilient Monitor

Before building a production monitor, validate the endpoint. Run the results table above, confirm that the endpoint returns pending hashes at a useful rate, and confirm that the filter survives your intended poll interval. If the endpoint does not maintain a public pending pool, consider a WebSocket subscription or a different provider. The Ethereum RPC node guide (RPC Assistant) and the OnFinality Learn hub are good starting points for comparing endpoint capabilities.

For production, wrap the monitor in a supervisor that recreates the filter on error, persists hashes before resolving them, and emits metrics for pending-hash rate, resolvability, and filter recreations. Treat the monitor as a best-effort discovery layer, not as a source of truth. Pair it with receipt polling for the transactions you actually care about.

If you are evaluating providers, the RPC pricing and API service pages describe the available plans and endpoints. The Ethereum network page lists the supported Ethereum networks. Choose an endpoint that documents its pending-pool behavior, and measure it yourself before committing to a design that depends on it.

  • Validate the endpoint with the results table before building.
  • Supervise the monitor: recreate filters, persist hashes, emit metrics.
  • Treat pending monitoring as best-effort discovery, not source of truth.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started