The blockWS and finalizedBlockWS WebSocket channels send real-time notifications when a new block is produced
or finalized.
Compared to polling the /chain/infoGET 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.
importasyncioimportjsonimportosfromwebsocketsimportconnectNODE_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}')asyncdefmain():asyncwithconnect(WS_URL)aswebsocket:# Connect to websocket endpointresponse=json.loads(awaitwebsocket.recv())uid=response['uid']print(f'Connected to {WS_URL} with uid {uid}')# Subscribe to block channelawaitwebsocket.send(json.dumps({'uid':uid,'subscribe':'block'}))print('Subscribed to block channel')# Subscribe to finalizedBlock channelawaitwebsocket.send(json.dumps({'uid':uid,'subscribe':'finalizedBlock'}))print('Subscribed to finalizedBlock channel')# Handle incoming messagestry:asyncforraw_messageinwebsocket:message=json.loads(raw_message)topic=message['topic']iftopic=='block':block=message['data']['block']block_meta=message['data']['meta']print(f'New block: height={int(block["height"]):,}'f' hash={block_meta["hash"][:16]}...')iftopic=='finalizedBlock':finalized=message['data']print(f'Finalized: height={int(finalized["height"]):,}'f' hash={finalized["hash"][:16]}...')# Unsubscribe on exitfinally:awaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':'block'}))awaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':'finalizedBlock'}))print('Unsubscribed from all channels')try:asyncio.run(main())exceptKeyboardInterrupt:passexceptExceptionaserror:print(error)
constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';constWS_URL=`${NODE_URL.replace('http','ws')}/ws`;console.log(`Using node ${NODE_URL}`);try{constwebsocket=newWebSocket(WS_URL);// Connect to websocket endpointconstuid=awaitnewPromise(resolve=>{websocket.addEventListener('message',event=>{constmessage=JSON.parse(event.data);resolve(message.uid);},{once:true});});console.log(`Connected to ${WS_URL} with uid ${uid}`);// Subscribe to block channelwebsocket.send(JSON.stringify({uid,subscribe:'block'}));console.log('Subscribed to block channel');// Subscribe to finalizedBlock channelwebsocket.send(JSON.stringify({uid,subscribe:'finalizedBlock'}));console.log('Subscribed to finalizedBlock channel');// Handle incoming messageswebsocket.addEventListener('message',event=>{constmessage=JSON.parse(event.data);consttopic=message.topic;if('block'===topic){constblock=message.data.block;constblockMeta=message.data.meta;console.log(`New block: height=${BigInt(block.height).toLocaleString()}`+` hash=${blockMeta.hash.substring(0,16)}...`);}if('finalizedBlock'===topic){constfinalized=message.data;console.log(`Finalized: height=${BigInt(finalized.height).toLocaleString()}`+` hash=${finalized.hash.substring(0,16)}...`);}});// Unsubscribe on exitprocess.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);}
//JAVA 21+//DEPS com.fasterxml.jackson.core:jackson-databind:2.17.1//DEPS org.glassfish.tyrus.bundles:tyrus-standalone-client:2.2.0importjava.io.IOException;importjava.net.URI;importjava.util.concurrent.CompletableFuture;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importjakarta.websocket.ClientEndpoint;importjakarta.websocket.ContainerProvider;importjakarta.websocket.OnMessage;importjakarta.websocket.RemoteEndpoint;importjakarta.websocket.Session;importjakarta.websocket.WebSocketContainer;@ClientEndpointpublicfinalclassListenNewBlocks{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatefinalCompletableFuture<String>uidFuture=newCompletableFuture<>();privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","https://reference.symboltest.net:3001");privatefinalStringwsUrl=nodeUrl.replaceFirst("http","ws")+"/ws";publicstaticvoidmain(finalString[]args){try{newListenNewBlocks().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsException{System.out.printf("Using node %s%n",nodeUrl);finalWebSocketContainercontainer=ContainerProvider.getWebSocketContainer();finalSessionsession=container.connectToServer(this,URI.create(wsUrl));finalRemoteEndpoint.Basicremote=session.getBasicRemote();// Connect to websocket endpointfinalStringuid=uidFuture.join();System.out.printf("Connected to %s with uid %s%n",wsUrl,uid);// Subscribe to block channelremote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("subscribe","block").toString());System.out.println("Subscribed to block channel");// Subscribe to finalizedBlock channelremote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("subscribe","finalizedBlock").toString());System.out.println("Subscribed to finalizedBlock channel");// Unsubscribe on exitRuntime.getRuntime().addShutdownHook(newThread(()->{try{remote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("unsubscribe","block").toString());remote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("unsubscribe","finalizedBlock").toString());System.out.println("Unsubscribed from all channels");session.close();}catch(finalIOExceptionex){thrownewIllegalStateException(ex);}}));// Wait foreverThread.currentThread().join();}// Handle incoming messages@OnMessagepublicvoidonMessage(finalStringpayload)throwsIOException{finalJsonNodemessage=JSON_MAPPER.readTree(payload);// Special case for the initial handshake messageif(message.has("uid")){uidFuture.complete(message.get("uid").asText());return;}finalStringtopic=message.get("topic").asText();if("block".equals(topic)){finalJsonNodeblock=message.get("data").get("block");finalJsonNodeblockMeta=message.get("data").get("meta");System.out.printf("New block: height=%,d hash=%s...%n",block.get("height").asLong(),blockMeta.get("hash").asText().substring(0,16));}if("finalizedBlock".equals(topic)){finalJsonNodefinalized=message.get("data");System.out.printf("Finalized: height=%,d hash=%s...%n",finalized.get("height").asLong(),finalized.get("hash").asText().substring(0,16));}}}
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.
asyncwithconnect(WS_URL)aswebsocket:# Connect to websocket endpointresponse=json.loads(awaitwebsocket.recv())uid=response['uid']print(f'Connected to {WS_URL} with uid {uid}')
constwebsocket=newWebSocket(WS_URL);// Connect to websocket endpointconstuid=awaitnewPromise(resolve=>{websocket.addEventListener('message',event=>{constmessage=JSON.parse(event.data);resolve(message.uid);},{once:true});});console.log(`Connected to ${WS_URL} with uid ${uid}`);
finalWebSocketContainercontainer=ContainerProvider.getWebSocketContainer();finalSessionsession=container.connectToServer(this,URI.create(wsUrl));finalRemoteEndpoint.Basicremote=session.getBasicRemote();// Connect to websocket endpointfinalStringuid=uidFuture.join();System.out.printf("Connected to %s with uid %s%n",wsUrl,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.
# Subscribe to block channelawaitwebsocket.send(json.dumps({'uid':uid,'subscribe':'block'}))print('Subscribed to block channel')# Subscribe to finalizedBlock channelawaitwebsocket.send(json.dumps({'uid':uid,'subscribe':'finalizedBlock'}))print('Subscribed to finalizedBlock channel')
// Subscribe to block channelwebsocket.send(JSON.stringify({uid,subscribe:'block'}));console.log('Subscribed to block channel');// Subscribe to finalizedBlock channelwebsocket.send(JSON.stringify({uid,subscribe:'finalizedBlock'}));console.log('Subscribed to finalizedBlock channel');
// Subscribe to block channelremote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("subscribe","block").toString());System.out.println("Subscribed to block channel");// Subscribe to finalizedBlock channelremote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("subscribe","finalizedBlock").toString());System.out.println("Subscribed to finalizedBlock channel");
The code subscribes to two channels:
blockWS: Notifies every time a new block is produced (approximately every 30 seconds).
finalizedBlockWS: 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.
// Handle incoming messages@OnMessagepublicvoidonMessage(finalStringpayload)throwsIOException{finalJsonNodemessage=JSON_MAPPER.readTree(payload);// Special case for the initial handshake messageif(message.has("uid")){uidFuture.complete(message.get("uid").asText());return;}finalStringtopic=message.get("topic").asText();if("block".equals(topic)){finalJsonNodeblock=message.get("data").get("block");finalJsonNodeblockMeta=message.get("data").get("meta");System.out.printf("New block: height=%,d hash=%s...%n",block.get("height").asLong(),blockMeta.get("hash").asText().substring(0,16));}if("finalizedBlock".equals(topic)){finalJsonNodefinalized=message.get("data");System.out.printf("Finalized: height=%,d hash=%s...%n",finalized.get("height").asLong(),finalized.get("hash").asText().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.
# Unsubscribe on exitfinally:awaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':'block'}))awaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':'finalizedBlock'}))print('Unsubscribed from all channels')
// Unsubscribe on exitprocess.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);});
// Unsubscribe on exitRuntime.getRuntime().addShutdownHook(newThread(()->{try{remote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("unsubscribe","block").toString());remote.sendText(JSON_MAPPER.createObjectNode().put("uid",uid).put("unsubscribe","finalizedBlock").toString());System.out.println("Unsubscribed from all channels");session.close();}catch(finalIOExceptionex){thrownewIllegalStateException(ex);}}));
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.
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.