WebSocket Provider
Basic Usage
import sh.brane.rpc.Brane;
// Create a WebSocket-based client
Brane client = Brane.builder()
.wsUrl("wss://ethereum.publicnode.com")
.build();
// Use like any other client
var latestBlock = client.getLatestBlock();
System.out.println("Block #" + latestBlock.number());
// Always close when done
client.close();For simple connection without additional configuration:
import sh.brane.rpc.Brane;
import sh.brane.rpc.WebSocketProvider;
// If you need direct provider access
WebSocketProvider provider = WebSocketProvider.create("wss://ethereum.publicnode.com");
Brane client = Brane.builder()
.provider(provider)
.build();Advanced Configuration
For production use cases, you'll want fine-grained control over connection behavior, timeouts, and performance tuning.
WebSocketConfig
For fine-grained control over WebSocket behavior, configure via WebSocketConfig:
import sh.brane.rpc.Brane;
import sh.brane.rpc.WebSocketConfig;
import sh.brane.rpc.WebSocketConfig.WaitStrategyType;
import sh.brane.rpc.WebSocketProvider;
import java.time.Duration;
var config = WebSocketConfig.builder("wss://eth.example.com")
.maxPendingRequests(32768) // Max concurrent requests
.ringBufferSize(8192) // Disruptor buffer (power of 2)
.waitStrategy(WaitStrategyType.YIELDING) // Low latency mode
.defaultRequestTimeout(Duration.ofSeconds(30))
.connectTimeout(Duration.ofSeconds(10))
.ioThreads(1) // Usually 1 is optimal
.writeIdleTimeout(Duration.ofSeconds(15)) // Ping if no writes
.readIdleTimeout(Duration.ofSeconds(30)) // Close if no reads
.maxFrameSize(4 * 1024 * 1024) // 4MB for large responses
.build();
WebSocketProvider provider = WebSocketProvider.create(config);
Brane client = Brane.builder()
.provider(provider)
.build();Configuration Reference
| Option | Default | Description |
|---|---|---|
maxPendingRequests | 65,536 | Maximum concurrent in-flight requests. Must be power of 2. |
ringBufferSize | 4,096 | Disruptor ring buffer size. Increase for high throughput. |
waitStrategy | YIELDING | YIELDING = low latency, high CPU. BLOCKING = low CPU, higher latency. |
defaultRequestTimeout | 60 sec | Timeout for requests without explicit timeout. |
connectTimeout | 10 sec | WebSocket handshake timeout. |
ioThreads | 1 | Netty I/O threads. 1 is usually optimal (avoids context switching). |
writeIdleTimeout | 15 sec | Send WebSocket ping if no data written for this duration. Keeps connection alive through NAT. |
readIdleTimeout | 30 sec | Close connection if no data received for this duration. Detects dead connections. |
maxFrameSize | 64 KB | Maximum WebSocket frame size. Range: 1 byte to 16 MB. Increase for large eth_getLogs responses. |
ringBufferSaturationThreshold | 0.10 | Warn via Metrics.onRingBufferSaturation() when buffer capacity falls below this fraction (0.0-1.0). |
Request Timeouts
Every async request has a configurable timeout to prevent hanging requests.
Default Timeout
The default timeout comes from WebSocketConfig.defaultRequestTimeout() (60 seconds by default):
// Uses the default timeout from config
var response = provider.sendAsync("eth_blockNumber", List.of()).get();Per-Request Timeout
Override the timeout for individual requests:
import java.time.Duration;
// Short timeout for time-sensitive operations
var gasPrice = provider.sendAsync("eth_gasPrice", List.of(), Duration.ofSeconds(5))
.get();
// Longer timeout for slow methods
var logs = provider.sendAsync("eth_getLogs", List.of(filter), Duration.ofSeconds(120))
.get();Connection Keepalive & Idle Timeouts
WebSocket connections can be silently dropped by NAT devices, firewalls, or load balancers after 30-60 seconds of inactivity. Brane automatically handles this with configurable idle timeouts.
How It Works
- Write idle: When no data has been written for
writeIdleTimeout, a WebSocket ping frame is sent to keep the connection alive through NAT devices. - Read idle: When no data has been received for
readIdleTimeout, the connection is considered dead and closed. The provider transitions toRECONNECTINGstate.
Configuration Examples
// Standard settings (defaults work for most cases)
var config = WebSocketConfig.builder("wss://...")
.writeIdleTimeout(Duration.ofSeconds(15)) // Ping every 15s of inactivity
.readIdleTimeout(Duration.ofSeconds(30)) // Close if no response for 30s
.build();
// Aggressive NAT environments (some mobile networks)
var config = WebSocketConfig.builder("wss://...")
.writeIdleTimeout(Duration.ofSeconds(10))
.readIdleTimeout(Duration.ofSeconds(20))
.build();
// Disable idle timeouts (not recommended)
var config = WebSocketConfig.builder("wss://...")
.writeIdleTimeout(Duration.ZERO)
.readIdleTimeout(Duration.ZERO)
.build();Subscriptions
WebSocket enables real-time event subscriptions via eth_subscribe.
New Block Headers
Subscribe to new blocks as they are mined:
String subscriptionId = provider.subscribe("newHeads", null, event -> {
System.out.println("New block: " + event);
});
// Later, unsubscribe
provider.unsubscribe(subscriptionId);Contract Event Logs
Subscribe to specific contract events:
import java.util.List;
import java.util.Map;
// Filter for Transfer events on USDC
Map<String, Object> filter = Map.of(
"address", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topics", List.of("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
);
String subscriptionId = provider.subscribe("logs", List.of(filter), event -> {
System.out.println("Transfer detected: " + event);
});Pending Transactions
Monitor the mempool for new pending transactions:
String subscriptionId = provider.subscribe("newPendingTransactions", null, event -> {
System.out.println("Pending tx: " + event);
});Custom Callback Executor
By default, subscription callbacks run on virtual threads to prevent blocking the Netty I/O thread.
For custom threading behavior:
import java.util.concurrent.Executors;
// Use a bounded thread pool for callbacks
provider.setSubscriptionExecutor(
Executors.newFixedThreadPool(4)
);
// Or run directly on the Netty I/O thread (use with caution)
provider.setSubscriptionExecutor(Runnable::run);High-Performance Batching
For high-throughput scenarios, use sendAsyncBatch() which uses an LMAX Disruptor ring buffer for optimal batching:
import java.util.concurrent.CompletableFuture;
import java.util.List;
// Fire many requests in rapid succession
CompletableFuture<?>[] futures = new CompletableFuture[1000];
for (int i = 0; i < 1000; i++) {
futures[i] = provider.sendAsyncBatch("eth_blockNumber", List.of());
}
// Wait for all responses
CompletableFuture.allOf(futures).join();This batches network writes together, reducing syscall overhead.
Async vs Batch Methods
| Method | Use Case | Latency | Throughput |
|---|---|---|---|
sendAsync() | Individual requests | Optimal | Good |
sendAsyncBatch() | Bulk requests | Good | Optimal |
Rule of thumb: Use sendAsync() normally. Switch to sendAsyncBatch() when sending many requests in a tight loop.
Backpressure & Error Handling
The provider has built-in backpressure to prevent overwhelming the connection.
Too Many Pending Requests
When you exceed maxPendingRequests, new requests fail immediately with an RpcException:
try {
var response = provider.sendAsync("eth_blockNumber", List.of()).get();
} catch (ExecutionException e) {
if (e.getCause() instanceof RpcException rpc) {
if (rpc.getMessage().contains("Too many pending requests")) {
// Back off and retry
Thread.sleep(100);
}
}
}Connection Lost
The provider attempts to reconnect automatically. If the connection is lost while requests are in-flight, those futures will complete exceptionally.
Connection State Machine
The WebSocketProvider exposes its connection state for monitoring and handling edge cases:
import sh.brane.rpc.WebSocketProvider;
import sh.brane.rpc.WebSocketProvider.ConnectionState;
var provider = WebSocketProvider.create("wss://...");
// Check current state
ConnectionState state = provider.getConnectionState();
System.out.println("Connection state: " + state);Connection States
| State | Description |
|---|---|
CONNECTING | Initial connection in progress |
CONNECTED | WebSocket is connected and operational |
RECONNECTING | Connection lost (network error or read idle timeout), attempting to reconnect |
CLOSED | Provider has been closed |
Handling Reconnection
During RECONNECTING state, requests may fail temporarily. For resilient applications:
ConnectionState state = provider.getConnectionState();
if (state == ConnectionState.RECONNECTING) {
// Wait or use fallback strategy
log.warn("WebSocket reconnecting, request may fail");
}
if (state == ConnectionState.CLOSED) {
// Provider is shut down, need a new instance
throw new IllegalStateException("Provider is closed");
}Client Types
The builder creates different client types based on configuration:
import sh.brane.rpc.Brane;
import sh.brane.core.crypto.PrivateKeySigner;
import sh.brane.core.builder.TxBuilder;
import sh.brane.core.types.Address;
import sh.brane.core.types.Wei;
import java.math.BigDecimal;
// Read-only client (Brane.Reader) - for queries and subscriptions
Brane.Reader reader = Brane.builder()
.wsUrl("wss://ethereum.publicnode.com")
.buildReader();
var balance = reader.getBalance(new Address("0x..."));
var sub = reader.onNewHeads(header -> System.out.println("Block: " + header.number()));
// Signing client (Brane.Signer) - can send transactions
Brane.Signer signer = Brane.builder()
.wsUrl("wss://ethereum.publicnode.com")
.signer(new PrivateKeySigner("0x..."))
.buildSigner();
// Create and send a transaction over WebSocket
var request = TxBuilder.eip1559()
.to(new Address("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"))
.value(Wei.fromEther(new BigDecimal("0.01")))
.build();
var receipt = signer.sendTransactionAndWait(request);When to Use WebSocket
| Scenario | Recommended |
|---|---|
| Real-time subscriptions | WebSocket |
| High-frequency trading / MEV | WebSocket |
| Long-running services | WebSocket |
| Simple read operations | HTTP or WebSocket |
| Serverless / Lambda | HTTP (no persistent connection) |
| Mobile apps | HTTP (battery efficiency) |