intermediate 45 min

Advanced RPC features

Subscriptions, WebSocket streams and gRPC — beyond simple request / response.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Implement subscription and streaming behaviour.
  • Understand the WebSocket and gRPC paths.
  • Handle advanced handler concerns.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈45 min · Intermediate · builds on Testing RPC handlers

Request-and-response is only the beginning. In this module you'll go further: subscriptions and WebSocket streams that push updates as they happen, batch requests, and the basics of the gRPC path. These are the features behind the real-time explorers, wallets and dashboards built on XRPL.

Polling versus push: a polling client sends five requests for one useful answer, while a subscribed WebSocket client sends one subscribe command and receives every ledgerClosed event the moment it happens.


Streaming Responses

In brief: send large results incrementally instead of all at once.

Streaming lets a handler send multiple response chunks over a single connection, useful for large result sets or real-time updates. Three patterns exist:

Pattern Transport When to use
Progress messages WebSocket long-running queries, keep the client informed
Pagination with a marker any large result sets, client-controlled pace
Chunked encoding HTTP large single responses without WebSocket

The pagination pattern is the one you will meet everywhere in rippled's own handlers (account_lines, account_objects, ledger_data):

What this code shows:

  • the server never holds state between pages: the marker carries the position
  • limits are always capped server-side, whatever the client asks for
  • the last page is signalled by the absence of a marker

Subscription Mechanisms

In brief: push updates to clients as ledgers, transactions, and validations happen.

The subscription lifecycle: a subscribe command registers an InfoSub entry, events are pushed as they happen, and the subscription ends by unsubscribe or disconnect; after any reconnect the client must subscribe again.

A client subscribes once, then receives typed push messages. The three core streams:

Stream Push message type Fired when
ledger ledgerClosed a ledger closes (index, hash, fees, txn count)
transactions / accounts transaction a transaction is applied (tx JSON + metadata)
validations validationReceived a validation message arrives from a validator

A ledgerClosed push looks like this:

{
    "type": "ledgerClosed",
    "ledger_index": 12345,
    "ledger_hash": "...",
    "fee_base": 12,
    "reserve_base": 10000000,
    "txn_count": 45
}

The subscribe handler, reduced to its skeleton

The real implementation lives in src/xrpld/rpc/handlers/subscribe/Subscribe.cpp; the shape is:

What matters here:

  • validation is all-or-nothing: one malformed account rejects the request
  • the handler never stores connection state itself; InfoSub does
  • unsubscribe mirrors the same shape and removes the entries

Key idea. Subscriptions flip the model: instead of the client polling, the node pushes updates as they happen. This is what powers live explorers and wallets.


Batch Request Processing

In brief: handle several commands in a single request.

A batch is simply an array of standard requests, each with its own id. Note that rippled itself does not accept JSON-RPC batch arrays natively: batching is a client- or proxy-side pattern, and the processor sketched below is what such a gateway implements:

[
    { "method": "account_info", "params": [{"account": "rN7n..."}], "id": 1 },
    { "method": "ledger", "params": [{"ledger_index": "validated"}], "id": 2 }
]

The processor loops over the array, dispatches each entry through the normal handler table, and collects the results:

Rules a good batch processor follows:

  • each entry gets its own error handling: one bad request must not fail the batch
  • results carry the caller's id so responses can be matched out of order
  • the ledger is resolved once and shared across entries (the big win)
  • the batch size counts against resource limits like any other request

gRPC Integration Basics

In brief: the gRPC path alongside JSON-RPC and WebSocket.

rippled also exposes a binary gRPC API, defined in protobuf under include/xrpl/proto/org/xrpl/rpc/v1. A service is a set of typed request/response pairs:

The server-side implementation is a thin adapter around the JSON handlers:

grpc::Status GetAccountInfo(grpc::ServerContext* ctx,
    GetAccountInfoRequest const* request,
    GetAccountInfoResponse* response) override
{
    // 1. Convert the protobuf request into Json::Value params
    // 2. Build an RPC::JsonContext and call doAccountInfo()
    // 3. Map a JSON error onto grpc::StatusCode::INVALID_ARGUMENT
    // 4. Copy the JSON fields into the typed protobuf response
    response->set_balance(result["balance"].asString());
    return grpc::Status::OK;
}

Three things to remember about the gRPC path:

  • it listens on its own port (configured in [port_grpc]), separate from HTTP/WS
  • handlers reuse the JSON implementations: gRPC is a translation layer, not a fork
  • clients get generated, typed stubs in any language from the same .proto files

Broadcasting Updates

When a ledger closes or a transaction applies, the server side walks the subscriber lists and pushes the typed message to every matching connection:

void broadcastLedgerClosed(std::shared_ptr<Ledger const> const& ledger,
                           NetworkOPs& netOps)
{
    Json::Value message;
    message["type"] = "ledgerClosed";
    message[jss::ledger_index] = ledger->info().seq;
    message["ledger_hash"] = to_string(ledger->hash());
    netOps.broadcastMessage(message);   // fan-out to subscribers
}

Transaction broadcasts work the same way, but fan out only to the connections subscribed to the affected accounts.


Performance Considerations

Advanced features multiply load, so every one of them ships with a guard rail:

Guard rail Typical value Protects against
Max items per streamed page 1,000-10,000 one query monopolising the node
Max subscriptions per connection ~100 subscriber list explosion
Slow-consumer drop transport-level one lagging client blocking the fan-out
Shared ledger per batch 1 lookup repeated ledger resolution

The pattern is always the same: cap the work per request, push the continuation cost back to the client (markers), and never let one connection degrade the others.


Summary

This module went beyond simple request and response. You saw subscriptions and WebSocket streaming, where the node pushes updates as ledgers, transactions, and validations happen, along with batch requests and the basics of the gRPC path. These are the features behind the real-time explorers, wallets, and dashboards built on XRPL.

To remember:

  • subscribe streams: ledger, transactions, validations, server, plus per-account variants
  • Push messages to know: ledgerClosed, transaction, validationReceived
  • Streams exist on WebSocket only, not HTTP
  • Batch = an array of requests processed in one round trip
  • gRPC services are proto-defined (org.xrpl.rpc.v1) on their own port
  • InfoSub is the server-side subscription machinery
  • Slow consumers get dropped: design clients to keep up or buffer
  • Watch out: subscriptions are per-connection; after a WebSocket reconnect you MUST re-subscribe

Next up. RPC closed. New phase, the heart of the system: before studying how nodes agree, look closely at the thing they agree ON. Next: ledger architecture and data structures.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026