Skip to content

Listening to New Blocks⚓︎

The block WS and finalizedBlock WS WebSocket channels send real-time notifications when a new block is produced or finalized. Compared to polling the /chain/info GET endpoint, WebSockets push updates as they happen without the overhead of repeated API calls.

This tutorial shows how to subscribe to both channels and display each update as it arrives.

Note

For a polling-based approach, see the Querying Chain and Finalization Height tutorial.

Prerequisites⚓︎

Install the websockets library:

pip install websockets

This tutorial uses the native WebSocket API available in Node.js 22 or later. No additional packages are required.

Full Code⚓︎

import asyncio
import json
import os

from websockets import connect

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')
WS_URL = NODE_URL.replace('http', 'ws', 1) + '/ws'
print(f'Using node {NODE_URL}')


async def main():
    async with connect(WS_URL) as websocket:
        # Connect to websocket endpoint
        response = json.loads(await websocket.recv())
        uid = response['uid']
        print(f'Connected to {WS_URL} with uid {uid}')

        # Subscribe to block channel
        await websocket.send(json.dumps(
            {'uid': uid, 'subscribe': 'block'}))
        print('Subscribed to block channel')

        # Subscribe to finalizedBlock channel
        await websocket.send(json.dumps(
            {'uid': uid, 'subscribe': 'finalizedBlock'}))
        print('Subscribed to finalizedBlock channel')

        # Handle incoming messages
        try:
            async for raw_message in websocket:
                message = json.loads(raw_message)
                topic = message['topic']

                if topic == 'block':
                    block = message['data']['block']
                    block_meta = message['data']['meta']
                    print(
                        f'New block: height={int(block["height"]):,}'
                        f' hash={block_meta["hash"][:16]}...'
                    )

                if topic == 'finalizedBlock':
                    finalized = message['data']
                    print(
                        f'Finalized: height={int(finalized["height"]):,}'
                        f' hash={finalized["hash"][:16]}...'
                    )

        # Unsubscribe on exit
        finally:
            await websocket.send(json.dumps(
                {'uid': uid, 'unsubscribe': 'block'}))
            await websocket.send(json.dumps(
                {'uid': uid, 'unsubscribe': 'finalizedBlock'}))
            print('Unsubscribed from all channels')


try:
    asyncio.run(main())
except KeyboardInterrupt:
    pass
except Exception as error:
    print(error)

Download source

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
const WS_URL = NODE_URL.replace('http', 'ws') + '/ws';
console.log(`Using node ${NODE_URL}`);

try {
    const websocket = new WebSocket(WS_URL);

    // Connect to websocket endpoint
    const uid = await new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const message = JSON.parse(event.data);
            resolve(message.uid);
        }, { once: true });
    });
    console.log(`Connected to ${WS_URL} with uid ${uid}`);

    // Subscribe to block channel
    websocket.send(JSON.stringify({ uid, subscribe: 'block' }));
    console.log('Subscribed to block channel');

    // Subscribe to finalizedBlock channel
    websocket.send(JSON.stringify({ uid, subscribe: 'finalizedBlock' }));
    console.log('Subscribed to finalizedBlock channel');

    // Handle incoming messages
    websocket.addEventListener('message', (event) => {
        const message = JSON.parse(event.data);
        const topic = message.topic;

        if (topic === 'block') {
            const block = message.data.block;
            const blockMeta = message.data.meta;
            console.log(
                'New block:'
                + ` height=${parseInt(block.height, 10).toLocaleString()}`
                + ` hash=${blockMeta.hash.substring(0, 16)}...`
            );
        }

        if (topic === 'finalizedBlock') {
            const finalized = message.data;
            console.log(
                'Finalized:'
                + ' height='
                + `${parseInt(finalized.height, 10).toLocaleString()}`
                + ` hash=${finalized.hash.substring(0, 16)}...`
            );
        }
    });

    // Unsubscribe on exit
    process.on('SIGINT', () => {
        websocket.send(JSON.stringify({ uid, unsubscribe: 'block' }));
        websocket.send(
            JSON.stringify({ uid, unsubscribe: 'finalizedBlock' }));
        console.log('Unsubscribed from all channels');
        websocket.close();
        process.exit(0);
    });
} catch (error) {
    console.error(error);
}

Download source

The snippet uses the NODE_URL environment variable to set the Symbol API node. If no value is provided, a default one is used. The WebSocket URL is derived from NODE_URL by replacing the HTTP protocol with the WebSocket protocol and appending /ws.

The program runs until interrupted with Ctrl+C, which triggers the unsubscribe step before closing the connection.

Code Explanation⚓︎

Connecting to the WebSocket⚓︎

    async with connect(WS_URL) as websocket:
        # Connect to websocket endpoint
        response = json.loads(await websocket.recv())
        uid = response['uid']
        print(f'Connected to {WS_URL} with uid {uid}')
    const websocket = new WebSocket(WS_URL);

    // Connect to websocket endpoint
    const uid = await new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const message = JSON.parse(event.data);
            resolve(message.uid);
        }, { once: true });
    });
    console.log(`Connected to ${WS_URL} with uid ${uid}`);

The first step is to open a WebSocket connection to the node's /ws endpoint. Upon connecting, the server sends a message containing a unique identifier (uid) that must be included in all subsequent subscription requests.

See the WebSocket reference for details on the connection protocol.

Subscribing to Channels⚓︎

        # Subscribe to block channel
        await websocket.send(json.dumps(
            {'uid': uid, 'subscribe': 'block'}))
        print('Subscribed to block channel')

        # Subscribe to finalizedBlock channel
        await websocket.send(json.dumps(
            {'uid': uid, 'subscribe': 'finalizedBlock'}))
        print('Subscribed to finalizedBlock channel')
    // Subscribe to block channel
    websocket.send(JSON.stringify({ uid, subscribe: 'block' }));
    console.log('Subscribed to block channel');

    // Subscribe to finalizedBlock channel
    websocket.send(JSON.stringify({ uid, subscribe: 'finalizedBlock' }));
    console.log('Subscribed to finalizedBlock channel');

The code subscribes to two channels:

  • block WS: Notifies every time a new block is produced (approximately every 30 seconds).
  • finalizedBlock WS: Notifies every time a finalization round completes (approximately every 10 to 20 minutes).

Each subscription message includes the uid received during the connection step and the name of the channel.

Handling Messages⚓︎

        # Handle incoming messages
        try:
            async for raw_message in websocket:
                message = json.loads(raw_message)
                topic = message['topic']

                if topic == 'block':
                    block = message['data']['block']
                    block_meta = message['data']['meta']
                    print(
                        f'New block: height={int(block["height"]):,}'
                        f' hash={block_meta["hash"][:16]}...'
                    )

                if topic == 'finalizedBlock':
                    finalized = message['data']
                    print(
                        f'Finalized: height={int(finalized["height"]):,}'
                        f' hash={finalized["hash"][:16]}...'
                    )
    // Handle incoming messages
    websocket.addEventListener('message', (event) => {
        const message = JSON.parse(event.data);
        const topic = message.topic;

        if (topic === 'block') {
            const block = message.data.block;
            const blockMeta = message.data.meta;
            console.log(
                'New block:'
                + ` height=${parseInt(block.height, 10).toLocaleString()}`
                + ` hash=${blockMeta.hash.substring(0, 16)}...`
            );
        }

        if (topic === 'finalizedBlock') {
            const finalized = message.data;
            console.log(
                'Finalized:'
                + ' height='
                + `${parseInt(finalized.height, 10).toLocaleString()}`
                + ` hash=${finalized.hash.substring(0, 16)}...`
            );
        }
    });

The code listens for incoming messages until the program is interrupted. Each message includes a topic field identifying the channel and a data object with the event payload.

For block messages, the payload follows the BlockInfoDTO schema. This tutorial uses two of them to identify each block:

  • data.block.height: The height of the new block.
  • data.meta.hash: The hash of the new block.

For finalizedBlock messages, the payload follows the FinalizedBlockDTO schema. This tutorial uses:

  • data.height: The finalized block height.
  • data.hash: The hash of the finalized block.

The chain height increases each time a new block is produced. The finalized height lags behind the chain tip because finalization typically occurs 10 to 20 minutes after block production.

See the Consensus section in the textbook for details on how voting nodes drive this process.

Unsubscribing on Exit⚓︎

        # Unsubscribe on exit
        finally:
            await websocket.send(json.dumps(
                {'uid': uid, 'unsubscribe': 'block'}))
            await websocket.send(json.dumps(
                {'uid': uid, 'unsubscribe': 'finalizedBlock'}))
            print('Unsubscribed from all channels')
    // Unsubscribe on exit
    process.on('SIGINT', () => {
        websocket.send(JSON.stringify({ uid, unsubscribe: 'block' }));
        websocket.send(
            JSON.stringify({ uid, unsubscribe: 'finalizedBlock' }));
        console.log('Unsubscribed from all channels');
        websocket.close();
        process.exit(0);
    });

When the program is interrupted (Ctrl+C), the code sends unsubscribe messages for both channels before closing the connection. This ensures a clean disconnection from the node.

Output⚓︎

The following output shows a typical run listening to new blocks and finalization events:

Using node https://reference.symboltest.net:3001
Connected to wss://reference.symboltest.net:3001/ws with uid 9AQEv+DFuCuddfrJlNh7ERf8Zlg=
Subscribed to block channel
Subscribed to finalizedBlock channel
New block: height=3,176,948 hash=EDD1ED4C92E29655...
New block: height=3,176,949 hash=18DBF7E75B2CC003...
New block: height=3,176,950 hash=A3409E1951E8FC8C...
Finalized: height=3,176,948 hash=EDD1ED4C92E29655...
New block: height=3,176,951 hash=A7D80AFD75C4C918...
New block: height=3,176,952 hash=027D939F6E9D0DAE...
Unsubscribed from all channels

The output shows:

  • Connection (line 2): The WebSocket connection is established to the wss:// URL and the server returns a unique uid.
  • Subscriptions (lines 3-4): Both the block and finalizedBlock channels are subscribed.
  • New blocks (lines 5-7, 9-10): New block notifications arrive approximately every 30 seconds.
  • Finalization (line 8): A finalization notification arrives when a finalization round completes, covering multiple blocks at once.
  • Unsubscribe (line 11): On Ctrl+C, the code unsubscribes from both channels.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Subscribe to block channel block WS
Subscribe to finalized channel finalizedBlock WS
Handle block messages BlockInfoDTO
Handle finalized messages FinalizedBlockDTO