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

Solana slotSubscribe and blockSubscribe Notification Mechanics

Understand how Solana's slotSubscribe and blockSubscribe streams emit notifications, why they are unsynchronised, and how to build a resilient consumer that reconciles gaps.

TL;DR

Solana's slotSubscribe and blockSubscribe WebSocket methods emit different notifications at different cadences: slotSubscribe fires once per produced slot with a SlotNotification carrying slot, parent, and root, while blockSubscribe fires per block with a BlockNotification containing the full block and an err field. The two streams are not synchronised—a slot notification typically precedes its block notification, root lags both, and some slots produce no block. Consumers must key correlation on the slot number, use root as a finality watermark, and reconcile gaps after reconnects using getBlocks or getBlock. This article explains the mechanics, provides a runnable Node.js example, and offers a troubleshooting playbook for building a reliable Solana notification consumer.

Notification Envelope and Subscription Lifecycle

Solana's WebSocket subscriptions follow the JSON-RPC 2.0 specification, where a notification is a JSON object with a jsonrpc field, a method field, and a params object containing a result and a subscription identifier. The subscription identifier is returned when the client first calls the subscribe method and must be used to match incoming notifications to the correct stream. This envelope is defined in the JSON-RPC 2.0 specification and is consistent across all Solana WebSocket methods.

When a client subscribes to slotSubscribe, the server responds with a subscription ID. Thereafter, the server pushes a notification for each new slot produced by the cluster. Similarly, blockSubscribe returns a subscription ID and pushes a notification for each block that matches the subscription parameters. The lifecycle ends when the client unsubscribes or the connection drops. Because notifications are server-initiated, the client must handle them asynchronously and maintain state to correlate slots and blocks.

The subscription ID is unique per connection and per method. If a client opens multiple subscriptions, it must route notifications by ID. A common mistake is to assume that notifications arrive in the order they were subscribed; the JSON-RPC 2.0 spec does not guarantee ordering across different subscriptions, and Solana's streams are independent.

  • Notification envelope: { jsonrpc: '2.0', method: 'slotNotification', params: { result: {...}, subscription: <id> } }
  • Subscription ID is returned by the initial slotSubscribe or blockSubscribe call.
  • Notifications are pushed asynchronously; the client must not block on them.

What slotSubscribe Emits: SlotNotification Payload and Cadence

The slotSubscribe method emits a SlotNotification for each slot the cluster produces. According to the Solana documentation for slotSubscribe, the notification result contains three fields: slot (the newly produced slot), parent (the parent slot), and root (the current root slot). There is no transaction data, no block content, and no indication of whether the slot contains a block. It is purely a liveness and ordering signal.

The cadence is approximately one notification per slot, but the exact timing depends on the cluster's slot production rate. Solana targets a slot time of around 400 milliseconds, but this can vary. Because slotSubscribe does not include block data, a consumer that treats a slot notification as 'a block is now readable' will often attempt to fetch a block that is not yet available or that will never be produced (if the slot is skipped).

The root field is the highest slot the cluster considers rooted, meaning it has been confirmed by a supermajority of stake. This is the correct watermark for safely pruning or finalising state. A consumer must not treat the notification's own slot as rooted; it is merely the latest slot observed.

  • Payload: { slot: number, parent: number, root: number }
  • Cadence: one notification per produced slot (approximately every 400ms).
  • Does not include transaction or block data.
  • root is the finality watermark, not the notification's slot.

What blockSubscribe Emits: BlockNotification and the err Field

The blockSubscribe method emits a BlockNotification for each block that matches the subscription criteria. The Solana documentation for blockSubscribe describes the notification result as containing slot, block, and err. The block field carries the full block content, including transactions, while err is either null (for a successful block) or an error object if the block was skipped or failed. This allows consumers to avoid a separate getBlock round trip when they need transaction data.

Because blockSubscribe only fires for blocks that are actually produced, it can legitimately skip slots that produced no block. A slot notification may arrive for a slot that never yields a block notification. This is a key difference: slotSubscribe reports every slot, while blockSubscribe reports only blocks. The err field is the signal that a block was not successfully produced, but its exact semantics depend on the subscription parameters and the cluster's behavior.

The blockSubscribe method requires parameters such as filter and commitment. The filter can be a string like all or an object with mentionsAccountOrProgram. The commitment level determines when the block is considered available. Higher commitment levels (e.g., finalized) may delay notifications but provide stronger guarantees. The notification's err field is not a substitute for checking the block's status; it indicates whether the block was skipped or failed.

  • Payload: { slot: number, block: object | null, err: object | null }
  • Cadence: one notification per produced block (not per slot).
  • Skips slots that produced no block.
  • err indicates a skipped or failed block; null means success.

Why the Two Streams Are Unsynchronised

The slotSubscribe and blockSubscribe streams are independent and not synchronised. A slot notification typically precedes its block notification because the slot is produced before the block is fully assembled and propagated. However, the ordering between a slot and its block notification is not guaranteed to be adjacent; other slot notifications may arrive in between. Therefore, any correlation between the two streams must key on the slot number rather than on arrival order.

Root lags both streams. The root field in a slot notification is the highest rooted slot, which is behind the current slot. This means that a block notification for a given slot may arrive before that slot is rooted. Consumers that require finality must wait for the root to advance past the slot of interest, or use a commitment level that reflects their risk tolerance.

Because the streams are unsynchronised, a consumer cannot assume that receiving a slot notification implies a block notification will follow. Some slots are skipped entirely, and some blocks may be delayed or never delivered due to network conditions. The only reliable way to correlate is to track the slot number and reconcile with getBlocks or getBlock when needed.

  • Slot notification typically precedes block notification, but not adjacently.
  • Root lags both streams; a block can be notified before its slot is rooted.
  • Correlate by slot number, not by arrival order.
  • Not every slot produces a block notification.

Using root from the Stream as a Finality Watermark

The root field in a SlotNotification is the highest slot the cluster considers rooted. This is the correct watermark for safely pruning or finalising state. A consumer should track the maximum root observed and use it to determine which slots are finalised. For example, if the current root is 1000, then slots up to 1000 are considered final and can be safely processed or pruned.

A common mistake is to treat the notification's own slot as rooted. The slot field is the newly produced slot, which is not yet rooted. Using it as a finality watermark would lead to processing unfinalised data. Instead, always use the root field. If the root stalls (does not advance for an extended period), it may indicate a network issue or a problem with the RPC provider.

When combining with blockSubscribe, a consumer can use the root from the slot stream to decide when a block is finalised. For instance, after receiving a block notification for slot N, wait until the root advances to at least N before considering the block final. This avoids acting on blocks that could be rolled back.

  • root is the highest rooted slot; use it as a finality watermark.
  • Do not treat the notification's slot as rooted.
  • Root stalling may indicate network or provider issues.
  • Combine root with block notifications to determine finality.

The Gap Problem: Reconnect and Skipped Slots

When the WebSocket connection drops, both slotSubscribe and blockSubscribe streams lose the slots and blocks observed while disconnected. Because some slots are skipped entirely, a consumer cannot infer a missed block from a slot-number jump alone. For example, if the last observed slot was 100 and the next is 105, slots 101-104 may have been produced but not observed, or some may have been skipped. Without additional data, the consumer cannot know which.

Recovery must reconcile against getBlocks or getBlock alongside the stream. After reconnecting, the consumer should query getBlocks for the range between the last observed slot and the current slot to determine which slots actually produced blocks. This is covered in detail in the article on detecting skipped slots and indexer gaps with getBlocks. The same principle applies to blockSubscribe: after a reconnect, fetch missing blocks using getBlock for the slots identified by getBlocks.

A robust consumer should also handle duplicate slot observations across a reconnect. If the client reconnects and resubscribes, it may receive notifications for slots it already processed. The consumer must deduplicate by slot number and maintain a consistent state. This is a common challenge in WebSocket-based systems, and the article on RPC WebSocket reconnect without data loss provides patterns for handling it.

  • Reconnect loses notifications for the disconnected period.
  • Slot-number jumps do not imply missed blocks; some slots are skipped.
  • Use getBlocks to reconcile the gap after reconnect.
  • Deduplicate slot observations to avoid double processing.

Runnable Node.js Consumer: Slot Tracking, Root Advancement, and Gap Detection

The following Node.js example connects to a Solana WebSocket endpoint, subscribes to slotSubscribe, records root advancement, counts slots observed versus blocks fetched, and reports the first gap after a forced reconnect. It uses the ws package for WebSocket communication and the @solana/web3.js library for HTTP fallback. Replace the endpoint with your own provider's WebSocket URL.

The example maintains a set of observed slots and a variable for the last root. On each slot notification, it updates the root and checks for gaps by comparing the new slot to the previous one. If a gap is detected, it queries getBlocks for the missing range. After a forced reconnect, it repeats the gap detection. This demonstrates the core mechanics of a resilient consumer.

const WebSocket = require('ws');
const { Connection, clusterApiUrl } = require('@solana/web3.js');

const WS_ENDPOINT = 'wss://api.mainnet-beta.solana.com'; // Replace with your provider
const HTTP_ENDPOINT = clusterApiUrl('mainnet-beta');
const connection = new Connection(HTTP_ENDPOINT, 'confirmed');

let ws;
let subscriptionId = null;
let lastSlot = null;
let lastRoot = null;
let observedSlots = new Set();
let reconnectCount = 0;

function connect() {
  ws = new WebSocket(WS_ENDPOINT);

  ws.on('open', () => {
    console.log('WebSocket connected');
    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'slotSubscribe',
      params: []
    }));
  });

  ws.on('message', async (data) => {
    const msg = JSON.parse(data);
    if (msg.method === 'slotNotification') {
      const { slot, parent, root } = msg.params.result;
      const subId = msg.params.subscription;
      if (subscriptionId === null) subscriptionId = subId;

      console.log(`Slot: ${slot}, Parent: ${parent}, Root: ${root}`);
      observedSlots.add(slot);

      if (lastRoot === null || root > lastRoot) {
        lastRoot = root;
        console.log(`Root advanced to ${root}`);
      }

      if (lastSlot !== null && slot > lastSlot + 1) {
        const gapStart = lastSlot + 1;
        const gapEnd = slot - 1;
        console.log(`Gap detected: slots ${gapStart} to ${gapEnd}`);
        try {
          const blocks = await connection.getBlocks(gapStart, gapEnd);
          console.log(`Blocks in gap: ${blocks.length}`);
        } catch (err) {
          console.error('Error fetching blocks:', err);
        }
      }
      lastSlot = slot;
    }
  });

  ws.on('close', () => {
    console.log('WebSocket closed. Reconnecting...');
    reconnectCount++;
    setTimeout(connect, 1000);
  });

  ws.on('error', (err) => {
    console.error('WebSocket error:', err);
  });
}

connect();

// Force a reconnect after 30 seconds for testing
setTimeout(() => {
  if (ws) ws.close();
}, 30000);

Results Table: Stream vs Payload vs Cadence vs What It Does Not Tell You

The following table summarises the key differences between slotSubscribe and blockSubscribe. Readers should verify these characteristics against their own endpoint, as provider-specific behavior may vary. For example, some providers may buffer or batch notifications, affecting cadence. The table is a guide for building a consumer, not a substitute for measurement.

To verify, run the Node.js example above and log the notifications. Compare the observed cadence to the expected slot time. Check whether block notifications arrive for every slot or only for produced blocks. Measure the lag between a slot notification and its corresponding block notification. Record the root advancement rate. These measurements will help you tune your consumer's timeout and reconciliation logic.

  • slotSubscribe: payload {slot, parent, root}, cadence per slot, does not tell you if a block exists.
  • blockSubscribe: payload {slot, block, err}, cadence per block, does not tell you about skipped slots.
  • Root lags both streams; use it as a finality watermark.
  • Provider-specific behavior: some providers may delay or batch notifications; verify with your own endpoint.

Troubleshooting: Block Notification Without Expected Block, Root Stalling, Duplicate Slots, Commitment Selection

If you receive a block notification but the block is not available via getBlock, it may be because the block was skipped or failed. Check the err field in the notification. If err is non-null, the block was not successfully produced. If err is null but getBlock returns null, the block may not yet be available at the requested commitment level. Try a lower commitment level or wait for the root to advance.

Root stalling—where the root field does not advance for an extended period—can indicate a network partition or a problem with the RPC provider. Monitor the root advancement rate and set an alert if it stalls beyond a threshold. If root stalls, consider switching to a different provider or endpoint. OnFinality's Solana network page provides information on supported endpoints and commitment levels.

Duplicate slot observations across a reconnect are common. When the client reconnects and resubscribes, it may receive notifications for slots it already processed. Deduplicate by maintaining a set of processed slots and ignoring duplicates. If you need to process each slot exactly once, use a persistent store to track processed slots across restarts.

Commitment selection affects when notifications are delivered. For blockSubscribe, the commitment parameter determines the level of finality required before a block is notified. Higher commitment levels (e.g., finalized) provide stronger guarantees but may increase latency. Choose a commitment level that matches your application's risk tolerance. The article on Solana commitment levels and transaction confirmation explains the tradeoffs in detail.

  • Block notification without block: check err field and commitment level.
  • Root stalling: monitor and consider switching providers.
  • Duplicate slots: deduplicate by slot number.
  • Commitment selection: balance finality and latency.

Limitations and Tradeoffs of WebSocket Subscriptions

WebSocket subscriptions are not a replacement for HTTP polling in all cases. They provide lower latency and push-based updates, but they are stateful and require careful handling of reconnects and gaps. If the connection drops, notifications are lost, and the consumer must reconcile. This adds complexity compared to polling, where the client controls the request cadence and can easily resume from the last processed slot.

Another limitation is that slotSubscribe does not include block data, and blockSubscribe does not include skipped slots. To get a complete picture, a consumer often needs both streams plus HTTP fallbacks. This increases resource usage and complexity. Additionally, provider-specific behavior can vary: some providers may limit the number of subscriptions, throttle notifications, or have different timeout policies. Always check your provider's documentation.

Finally, the root field is a cluster-level watermark, but it does not guarantee that a specific block is finalised. A block may be rooted but later rolled back in rare cases. For most applications, rooted is sufficient, but for high-value transactions, additional confirmation may be needed. The Solana documentation provides the authoritative details on notification semantics.

  • WebSocket subscriptions are stateful; reconnects require reconciliation.
  • No single stream provides complete data; combine slot, block, and HTTP.
  • Provider-specific limits and throttling may apply.
  • Rooted does not guarantee absolute finality in all edge cases.

Next Steps: Building a Production-Ready Consumer

To build a production-ready consumer, start by implementing the Node.js example and measuring the notification cadence and root advancement on your chosen endpoint. Use the results to set timeouts and reconciliation intervals. Then, add persistent storage for processed slots to handle restarts and deduplication. Integrate getBlocks and getBlock for gap recovery, as described in the getBlocks gap detection article.

Consider using a provider that offers reliable WebSocket endpoints and clear documentation. OnFinality's Solana WebSocket API provides a starting point for understanding the available methods. For pricing and service details, see RPC pricing and API service. The OnFinality Learn hub contains more articles on Solana reliability and consistency.

Finally, test your consumer under adverse conditions: force reconnects, simulate network latency, and verify that gap recovery works. Monitor root advancement and alert on stalls. With these practices, you can build a resilient Solana notification consumer that handles the unsynchronised nature of slotSubscribe and blockSubscribe.

  • Measure cadence and root advancement on your endpoint.
  • Implement persistent deduplication and gap recovery.
  • Choose a reliable provider and understand its limits.
  • Test under reconnects and network latency.

Never Worry about Infrastructure Again

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

Get Started