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.