eth_newFilter: Poll for Logs Without WebSockets

eth_newFilterRead methodEthereum JSON-RPC

eth_newFilter registers a log filter inside the node and returns an id. You then poll that id with eth_getFilterChanges and receive only the logs that arrived since your last poll. It is the pre-WebSocket way to follow events, and it is still the only way to do it over plain HTTP, at the price of being stateful on a node you do not control.

1 credit
per call on BLAZED.sh
Read
call type
1
parameter
All clients
client support

Try eth_newFilter

Request as curl
curl -s https://ethereum-rpc.publicnode.com \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_newFilter",
  "params": [
    {
      "fromBlock": "latest",
      "toBlock": "latest",
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
      ]
    }
  ]
}'

Runs the example straight from your browser against the endpoint above. The prefilled URL is a third-party public endpoint, not run by BLAZED.sh.

What eth_newFilter does

The filter object is the same one eth_getLogs takes: fromBlock, toBlock, address and topics, minus blockHash. The node stores it, remembers where you last read, and advances that cursor every time you poll. On Geth a filter created with a fromBlock in the past returns that backlog on the first poll and only new logs afterwards, which makes "backfill then follow" a single object rather than two code paths.

Three companions complete the API. eth_getFilterChanges returns what is new since the last poll and moves the cursor. eth_getFilterLogs returns everything matching the filter regardless of the cursor, and only works for filters created with eth_newFilter. eth_uninstallFilter deletes the filter, which is worth doing explicitly rather than waiting for the timeout. Two sibling constructors, eth_newBlockFilter and eth_newPendingTransactionFilter, produce id-based streams of block hashes and pending transaction hashes with the same polling model.

The critical thing to understand is that a filter is state living on one specific node. It is not part of the chain, it cannot be recreated from its id, and it disappears when the node forgets it. Everything that goes wrong with filters in production follows from that one fact.

Parameters

#NameTypeDescription
1filterObjectobjectfromBlock and toBlock (hex block numbers or latest, safe, finalized, earliest), address (one address or an array), and topics using the same positional, nullable, OR-capable matching as eth_getLogs. blockHash is not accepted here.

Omitting fromBlock and toBlock gives you a filter that starts at the current head and follows it. Setting toBlock to a fixed block gives a filter that stops producing once the chain passes it, which is a quiet way to build a poller that mysteriously goes silent.

What it returns

A hex filter id such as 0x6c4e010000000000d4d17d7505f071cf. The id is opaque, node-local and only meaningful to the node that issued it.

Polling it with eth_getFilterChanges returns an array of log objects in the same shape eth_getLogs produces, including removed: true entries for logs from blocks that were reorged out. An empty array means nothing matched since the last poll, which is the normal answer most of the time.

Example response

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": "0x6c4e010000000000d4d17d7505f071cf"
}

The filter id. Poll it with eth_getFilterChanges using the same endpoint: {"jsonrpc":"2.0","id":1,"method":"eth_getFilterChanges","params":["0x6c4e010000000000d4d17d7505f071cf"]}. Ids from a run of the example above are yours alone and expire quickly.

eth_newFilter with ethers.js

import { JsonRpcProvider, id } from "ethers";

const provider = new JsonRpcProvider("https://ethereum-rpc.publicnode.com");

const filterId = await provider.send("eth_newFilter", [
  {
    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    topics: [id("Transfer(address,address,uint256)")],
  },
]);

// poll well inside the node's idle timeout, and expect it to vanish anyway
setInterval(async () => {
  try {
    const logs = await provider.send("eth_getFilterChanges", [filterId]);
    for (const log of logs) console.log(log.blockNumber, log.transactionHash);
  } catch (err) {
    console.error("filter gone, recreate it:", err.shortMessage ?? err);
  }
}, 6_000);

// when you are done
// await provider.send("eth_uninstallFilter", [filterId]);

Gotchas and common errors

Load-balanced endpoints break filters

A hosted URL usually fronts a pool of nodes. Your eth_newFilter lands on one of them, and the next poll can be routed to a different one that has never heard of the id, which comes back as "filter not found". Nothing is wrong with your code; the pattern is simply incompatible with stateless load balancing unless the provider pins sessions. This is the single most common reason filters work in local development and fail in production.

Filters expire when you stop polling

Nodes garbage-collect idle filters; Geth's deadline is five minutes without a poll. A worker that pauses, a process that is suspended between cron runs, or a poll interval tuned longer than the timeout all end with "filter not found". Treat that error as normal: recreate the filter from your last processed block rather than crashing.

eth_getFilterChanges and eth_getFilterLogs are not the same call

getFilterChanges is incremental and moves the cursor, so calling it twice in a row returns the second call's logs only, and a poll you lose to a network error is a poll you cannot repeat. getFilterLogs re-returns everything the filter matches from its fromBlock, which is expensive but idempotent. Use changes for the steady state and logs for recovery, and keep your own last-processed block number so you can always fall back to eth_getLogs.

Polling costs requests whether or not anything happened

Every poll is a billable request that usually returns an empty array. Polling every second on a chain that produces a block every twelve seconds means eleven wasted requests out of twelve. If your transport can hold a connection open, eth_subscribe delivers the same logs as they arrive with no polling at all; filters exist for the cases where it cannot.

Reorgs arrive as removed entries

When a block is reorged out, its logs come back through the filter with removed set to true. Consumers that ignore the flag double-count events; consumers that filter those entries out silently keep data that is no longer on chain. Handle them as deletions, or stay behind the finalized tag where they cannot occur.

What eth_newFilter costs on BLAZED.sh

eth_newFilter costs 1 credit on BLAZED.sh, and so does each eth_getFilterChanges poll, which is where the real spend sits: the cost of this pattern is the polling loop, not the filter. One poll per block is the natural cadence, since logs cannot appear between blocks. Where a persistent connection is possible, a WebSocket subscription replaces the whole loop with pushes; inside a BLAZED.sh container that connection is a local socket to the node on the same host, so there is rarely a reason to poll at all.

See the full credit price list

eth_newFilter: frequently asked questions

Why do I get "filter not found"?

The node holding your filter no longer has it: it expired after an idle period (five minutes on Geth), the node restarted, or a load balancer routed your poll to a different node in the pool. Recreate the filter and resume from your last processed block.

What is the difference between eth_newFilter and eth_subscribe?

eth_newFilter is polling over any transport, with the node keeping a cursor for you. eth_subscribe is push over WebSocket or IPC, with no polling and lower delay. Use subscriptions when you can hold a connection, filters when you are limited to HTTP.

How often should I poll a filter?

About once per block, so every twelve seconds or so, and always well inside the node's idle timeout. Faster polling multiplies requests without producing new data, since logs only exist once a block is imported.

Do I need to call eth_uninstallFilter?

Not strictly, since idle filters are collected automatically, but doing it explicitly frees node resources immediately and is good manners on a shared endpoint. It is also the clean way to end a subscription-like loop.

Can I recover logs I missed while my poller was down?

Not through the filter, which will have expired. Keep the last processed block number and backfill the gap with eth_getLogs over that range, then create a fresh filter from the head.

Call eth_newFilter 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.