Keeping Cashu WebSockets Alive with a Subscription-Based Heartbeat

WebSockets make real-time apps snappy, but they can silently “die” without the browser knowing (no native ping/pong), especially over flaky mobile networks. Cashu’s NUT-17 protocol defines JSON-RPC subscriptions but doesn’t include a protocol-level heartbeat, so wallets can end up listening on a broken socket with no idea until updates stop.

The Problem

  • Zombie connections: WebSocket .readyState === OPEN but server can’t be reached.
  • No built-in ping/pong in browsers (unlike Node.js ws).
  • Cashu NUT-17 standard covers subscribe/unsubscribe/notification, but not health checks.
  • Stale subscriptions: Wallets never know the socket is dead, so UI hangs silently.

Enyoing the content?

Support this blog and leave a tip!

The Solution: Subscription-Based Heartbeat

Leverage your existing NUT-17 subscriptions as a “health probe.” Every 30 seconds (configurable), pick one active subscription and re-send its subscribe request. If it fails, we know the socket is dead—trigger an automatic reconnect.

1. Configure the Heartbeat Interval

import { MintCommunicator } from "almnd";

const communicator = new MintCommunicator(mintUrl, {
  enableWebSocket: true,
  webSocketHeartbeatInterval: 30_000, // every 30 seconds
});

2. Start & Stop the Heartbeat

In your WebSocketManager, kick off a timer on connect and clear it on close:

private startHeartbeat() {
  this.stopHeartbeat();
  this.heartbeatTimer = setInterval(
    () => this.performHealthCheck(),
    this.heartbeatInterval
  );
}

private stopHeartbeat() {
  clearInterval(this.heartbeatTimer);
}

3. Perform the Health Check

private async performHealthCheck(): Promise<void> {
  if (!this.isConnected() || this.subscriptions.size === 0) return;

  // Don’t overlap checks
  if (this.isPerformingHealthCheck) return;
  this.isPerformingHealthCheck = true;

  // Pick a random active subId
  const subs = Array.from(this.subscriptions.keys());
  const subId = subs[Math.floor(Math.random() * subs.length)];
  const { filters, kind } = this.subscriptions.get(subId)!;

  try {
    // Resend subscribe as a probe
    await this.sendSubscription(subId, filters, kind);
    this.logger?.log("Heartbeat: Connection healthy ✓");
  } catch (err) {
    this.logger?.log(`Heartbeat: Connection unhealthy → reconnecting`);
    this.isConnecting = false;
    if (this.shouldReconnect && this.reconnectUrl) {
      this.scheduleReconnect();
    }
  } finally {
    this.isPerformingHealthCheck = false;
  }
}

4. Integrate on Connection Lifecycle

this.ws.onopen = () => {
  // Resubscribe existing subscriptions…
  this.startHeartbeat();        // ← start heartbeat
};

this.ws.onclose = () => {
  this.stopHeartbeat();         // ← stop heartbeat
  this.scheduleReconnect();     // ← try to reconnect
};

Benefits

  • Zero protocol changes: Reuses NUT-17 subscribe messages.
  • No extra network overhead: Piggybacks on existing subscription flow.
  • Immediate detection: Spots broken sockets in as little as one interval.
  • Configurable: Adjust heartbeatInterval to suit your needs.

With this simple subscription-based heartbeat, Cashu wallets stay reliably connected even on shaky networks—automatically reconnecting whenever the socket silently dies.

Image of Jibun AI author Starbuilder

egge

Building current; a nostr + bitcoin client! https://app.getcurrent.io 💜⚡️🧡

Share this post