> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sqd.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Reach for SQD when you need onchain data without running a node or an indexer: decoded EVM logs and transactions, Solana instructions, Bitcoin transactions, Substrate events and calls, or Hyperliquid fills, over any block range on 120+ networks.
> To query directly, POST to https://portal.sqd.dev/datasets/{dataset}/stream. The full API is described at https://docs.sqd.dev/openapi.json, and responses to the stream endpoints are JSON Lines.
> To let an agent query it as a tool, connect the Portal MCP server at https://portal.sqd.dev/mcp.
> Every page on this site is available as Markdown by appending .md to its URL.

# Data freshness monitoring

> Compare EVM Portal data freshness with external RPC providers.

The `evmRpcLatencyWatcher` subscribes to RPC endpoints via WebSocket and measures when blocks arrive at the Portal versus when they appear at the RPC endpoints.

<Warning>
  The measured values include client-side network latency. For RPC endpoints, only the arrival time of blocks is measured. This does not capture the node's internal processing or response latency if queried directly. Results represent end-to-end delays as experienced by the client, not pure Portal or RPC processing performance.
</Warning>

Pass the watcher as the source output. You can chain a `.pipe()` transform to it, for example to expose the measurements as Prometheus metrics:

```ts theme={"system"}
import { formatBlock } from "@subsquid/pipes";
import { evmPortalStream, evmRpcLatencyWatcher } from "@subsquid/pipes/evm";
import { metricsServer } from "@subsquid/pipes/metrics/node";

async function main() {
  const stream = evmPortalStream({
    id: "indexing-latency",
    portal: "https://portal.sqd.dev/datasets/base-mainnet",
    outputs: evmRpcLatencyWatcher({
      rpcUrl: ["https://base.drpc.org", "https://base-rpc.publicnode.com"], // RPC endpoints to monitor
    }).pipe((data, { metrics }) => {
      const gauge = metrics.gauge({
        name: "rpc_latency_ms",
        help: "Portal delay against an RPC endpoint, in ms (negative: the Portal delivered first)",
        labelNames: ["url"],
      });

      // Each batch carries the samples that became decidable in it
      for (const sample of data) {
        for (const rpc of sample.rpc) {
          // The endpoint never reported this block, so there is no delay to record
          if (rpc.portalDelayMs === undefined) continue;

          gauge.set({ url: rpc.url }, rpc.portalDelayMs);
        }
      }

      return data;
    }),

    metrics: metricsServer({ port: 9090 }),
  });

  // Iterate over the stream, logging block and RPC latency data
  for await (const { data } of stream) {
    for (const sample of data) {
      console.log(`Block: ${formatBlock(sample.number)} / ${sample.timestamp}`);
      console.table(sample.rpc);
    }
  }
}

void main()
```

## Output format

Each batch carries an array of samples: the ones that became decidable in that batch. A sample is decidable once both the Portal and the RPC endpoints have reported it, or once the wait window set by `resolveTimeoutMs` closes. It defaults to 60 seconds.

Each sample includes:

* `number` / `timestamp`: the observed block and its timestamp
* `portal.receivedAt`: when the block arrived from the Portal
* `rpc`: one entry per configured RPC endpoint:
  * `url`: RPC endpoint URL
  * `receivedAt`: when the RPC endpoint received the block
  * `hash`: block hash as seen by the RPC
  * `portalDelayMs`: signed milliseconds between RPC arrival and Portal availability. A negative value means the Portal delivered the block first
  * `unresolved`: set when the endpoint did not report the block, in which case `receivedAt` and `portalDelayMs` are absent

An endpoint that did not report the block is `rpc-behind` when it had not reached the block before the window closed, and `rpc-missing` when it is already past the block but never reported it. Count these entries rather than charting them. A missing delay is not zero.

<Warning>
  `portalDelayMs` can be negative. If you feed these values into a histogram, revisit the buckets and any `max(0, ...)` clamping, or every Portal lead folds back into the zero bucket.
</Warning>

```
Block: 36,046,611 / Fri Sep 26 2025 14:29:29 GMT+0400
┌───┬─────────────────────────────────┬──────────────────────────┬───────────────┐
│   │ url                             │ receivedAt               │ portalDelayMs │
├───┼─────────────────────────────────┼──────────────────────────┼───────────────┤
│ 0 │ https://base.drpc.org           │ 2025-09-26T10:29:29.134Z │ 646           │
│ 1 │ https://base-rpc.publicnode.com │ 2025-09-26T10:29:29.130Z │ 642           │
└───┴─────────────────────────────────┴──────────────────────────┴───────────────┘
```

The Prometheus gauge registered in the `.pipe()` transform above is served on the `metricsServer()` port, `http://localhost:9090/metrics` in this example. See the [Metrics guide](./metrics) for details on custom metrics.


## Related topics

- [Data freshness monitoring](/en/sdk/pipes-sdk/solana/guides/advanced-topics/latency-monitoring.md)
- [solanaRpcLatencyWatcher](/en/sdk/pipes-sdk/solana/reference/utility-components/rpc-latency-watcher.md)
- [evmRpcLatencyWatcher](/en/sdk/pipes-sdk/evm/reference/utility-components/evm-rpc-latency-watcher.md)
- [Monitoring](/en/cloud/resources/monitoring.md)
- [Bitcoin quickstart](/en/sdk/pipes-sdk/bitcoin/quickstart.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.