> ## 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 130+ 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.

# solanaRpcLatencyWatcher

> Monitor RPC latency and compare with Portal performance

The `solanaRpcLatencyWatcher` function monitors the latency between Portal and external RPC providers, helping you track indexing performance.

## Import

```ts theme={"system"}
import { solanaRpcLatencyWatcher } from '@subsquid/pipes/solana'
```

## Signature

```ts theme={"system"}
function solanaRpcLatencyWatcher(options: {
  rpcUrl: string[]
  resolveTimeoutMs?: number
}): Transformer
```

## Parameters

| Parameter          | Type       | Description                                                                                                 |
| ------------------ | ---------- | ----------------------------------------------------------------------------------------------------------- |
| `rpcUrl`           | `string[]` | Array of RPC endpoint URLs to monitor                                                                       |
| `resolveTimeoutMs` | `number`   | How long to wait for an endpoint to report a slot before recording it as `rpc-behind`. Defaults to `60_000` |

## Return Value

Returns a query-transformer combo that already includes the block query it needs. Pass it directly as the source's `outputs`. You can chain `.pipe()` transforms to it.

## Output Data Structure

Each batch carries the samples that became decidable in it, so iterate the result:

```ts theme={"system"}
type LatencySample = {
  number: number,           // Slot number
  timestamp: Date,          // Block timestamp
  portal: {
    receivedAt: Date,       // When the block arrived from the Portal
  },
  rpc: Array<{
    url: string,            // RPC endpoint URL
    hash?: string,          // Not set on Solana: slot update notifications carry no hash
    receivedAt?: Date,      // When the block was received from this RPC
    portalDelayMs?: number, // Signed delay; negative means the Portal delivered first
    unresolved?: 'rpc-behind' | 'rpc-missing',
  }>
}
```

A sample is emitted once both the Portal and the RPC endpoints have reported the slot, or once the wait window closes. `rpc` carries one entry per configured endpoint on every sample.

`portalDelayMs` is signed. A negative value means the Portal delivered the slot before that endpoint did. If these values feed a histogram, revisit the buckets and any `max(0, ...)` clamping, or Portal leads fold back into the zero bucket.

`portalDelayMs` and `receivedAt` are absent whenever `unresolved` is set:

| `unresolved`  | Meaning                                                                                                                                        |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `rpc-behind`  | The endpoint had not reached the slot before the wait window closed, so the Portal is ahead by at least that window.                           |
| `rpc-missing` | The endpoint is already past the slot but never reported it, after a reorg, a dropped update, or a slot seen while the stream was backfilling. |

Count unresolved entries rather than charting them. A missing delay is not zero.

## Basic Usage

```ts theme={"system"}
import { solanaPortalStream, solanaRpcLatencyWatcher } from '@subsquid/pipes/solana'

const stream = solanaPortalStream({
  id: 'solana-latency',
  portal: 'https://portal.sqd.dev/datasets/solana-mainnet',
  outputs: solanaRpcLatencyWatcher({
    rpcUrl: ['https://api.mainnet-beta.solana.com'],
  }),
})

for await (const { data } of stream) {
  for (const sample of data) {
    console.log(`Slot: ${sample.number}`)
    console.table(sample.rpc)
  }
}
```

## Example Output

```
-------------------------------------
BLOCK DATA: 369,377,455 / Fri Sep 26 2025 15:31:36 GMT+0400
┌───┬─────────────────────────────────────┬──────────────────────────┬───────────────┐
│   │ url                                 │ receivedAt               │ portalDelayMs │
├───┼─────────────────────────────────────┼──────────────────────────┼───────────────┤
│ 0 │ https://api.mainnet-beta.solana.com │ 2025-09-26T11:31:37.075Z │ 358           │
└───┴─────────────────────────────────────┴──────────────────────────┴───────────────┘
```

## Multiple RPC Endpoints

Monitor multiple RPC providers simultaneously:

```ts theme={"system"}
const stream = solanaPortalStream({
  id: 'solana-latency',
  portal: 'https://portal.sqd.dev/datasets/solana-mainnet',
  outputs: solanaRpcLatencyWatcher({
    rpcUrl: [
      'https://api.mainnet-beta.solana.com',
      'https://solana-mainnet.rpc.extrnode.com',
    ],
  }),
})
```

## Metrics Integration

Export latency data to Prometheus via the built-in metrics registry. Skip the entries that carry `unresolved`, since they have no delay to record. See the [Data freshness monitoring guide](../../guides/advanced-topics/latency-monitoring) for the full example:

```ts theme={"system"}
import { solanaPortalStream, solanaRpcLatencyWatcher } from '@subsquid/pipes/solana'
import { metricsServer } from '@subsquid/pipes/metrics/node'

const stream = solanaPortalStream({
  id: 'solana-latency',
  portal: 'https://portal.sqd.dev/datasets/solana-mainnet',
  outputs: solanaRpcLatencyWatcher({
    rpcUrl: ['https://api.mainnet-beta.solana.com'],
  }).pipe({
    profiler: { name: 'expose metrics' },
    transform: (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'],
      })

      for (const sample of data) {
        for (const rpc of sample.rpc) {
          if (rpc.portalDelayMs === undefined) continue

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

      return data
    },
  }),
  metrics: metricsServer({ port: 9090 }),
})
```

## How It Works

The RPC latency watcher:

1. Subscribes to slot updates via WebSocket (`slotsUpdatesSubscribe`)
2. Listens for `optimisticConfirmation` events from each RPC
3. Holds each slot until both sides have reported it, or until `resolveTimeoutMs` passes
4. Reports the signed delay as `portalDelayMs`, or `unresolved` when an endpoint never reported the slot

<Warning>
  The measured values include client-side network latency. For RPC, only the arrival time of the block is measured. This does not capture the node's internal processing latency.
</Warning>

## Use Cases

* **Performance monitoring** - Track indexing latency in production
* **RPC comparison** - Compare performance across different RPC providers
* **Alerting** - Trigger alerts when latency exceeds thresholds
* **Optimization** - Identify bottlenecks in your indexing pipeline


## Related topics

- [Data freshness monitoring](/en/sdk/pipes-sdk/solana/guides/advanced-topics/latency-monitoring.md)
- [evmRpcLatencyWatcher](/en/sdk/pipes-sdk/evm/reference/utility-components/evm-rpc-latency-watcher.md)
- [Bitcoin portal stream](/en/sdk/pipes-sdk/bitcoin/reference/basic-components/source.md)
- [Pipes SDK 1.0](/announcements/pipes-sdk-1-0.md)
- [Changelog](/changelog.md)
