WS Compression
WebSocket Compression
WebSocket compression reduces the size of messages sent over a WebSocket connection, making data transmission more efficient. This is particularly valuable when sending large amounts of repetitive or structured data like JSON payloads, log entries, or real-time updates.
Why it matters: Compression can dramatically reduce bandwidth usage and improve performance, especially for applications that send verbose data formats or operate in bandwidth-constrained environments. We've measured the bandwidth savings of 70-90% for typical JSON WebSocket messages when using compression.
How it works: Compression extensions are negotiated during the WebSocket handshake. If both client and server support the same compression method, messages are automatically compressed before transmission and decompressed upon receipt. Without compression, WebSocket frames are sent as raw, uncompressed data.
This is achieved by supporting the Per-Message Deflate extension, which is the standard for WebSocket compression.
Per-Message Deflate
Per-Message Deflate (permessage-deflate
) is the standard WebSocket compression extension, defined in RFC 7692. It uses the DEFLATE algorithm—the same compression method found in gzip and ZIP files—to compress individual WebSocket messages.
We support per-message deflate on all WebSocket subscriptions, including Solana RPC and Chainstream.
WebSocket clients that support per-message deflate
Here's a non-exhaustive list of popular WebSocket libraries that support per-message deflate:
- ws (Node.js)
- websockets (Python)
- gorilla/websocket (Go)
- ratchet_rs (Rust)
Implementation Examples
- Python
- Go
- Javascript
import asyncio
import signal
import websockets
from websockets.asyncio.client import connect
async def client():
# NOTE: by default permessage-deflate is enabled
async with connect("wss://api.syndica.io/api-key/<YOUR_API_KEY>") as websocket:
print("CONNECTED")
await websocket.send("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"solana-mainnet.slotSubscribe\"}")
# Close the connection when receiving SIGTERM.
loop = asyncio.get_running_loop()
loop.add_signal_handler(signal.SIGTERM, loop.create_task, websocket.close())
# Process messages received on the connection.
async for message in websocket:
print("Received message:", message)
asyncio.run(client())
package main
import (
"flag"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"time"
"github.com/gorilla/websocket"
)
var addr = flag.String("addr", "api.syndica.io", "http service address")
var path = flag.String("path", "/api-key/<YOUR_API_KEY>", "websocket path")
func main() {
flag.Parse()
log.SetFlags(0)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
u := url.URL{Scheme: "wss", Host: *addr, Path: *path}
log.Printf("connecting to %s", u.String())
dialer := websocket.DefaultDialer
// enable/disable compression here
// default value for sec-websocket-extensions is: `permessage-deflate; server_no_context_takeover; client_no_context_takeover`
dialer.EnableCompression = true
c, response, err := dialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("dial:", err)
}
fmt.Printf("Response: %+v\n", response)
defer c.Close()
done := make(chan struct{})
go func() {
defer close(done)
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Println("read:", err)
return
}
log.Printf("recv: %s", message)
}
}()
msg := `{"jsonrpc": "2.0","id": "TEST","method": "solana-mainnet.slotSubscribe"}`
subErr := c.WriteMessage(websocket.TextMessage, []byte(msg))
if subErr != nil {
log.Println("error subscribing:", subErr)
return
}
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
pingErr := c.WriteMessage(websocket.PingMessage, []byte(""))
if pingErr != nil {
log.Println("error writing ping message:", pingErr)
return
}
case <-interrupt:
log.Println("interrupt")
// Cleanly close the connection by sending a close message and then
// waiting (with timeout) for the server to close the connection.
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("write close:", err)
return
}
select {
case <-done:
case <-time.After(time.Second):
}
return
}
}
}
try {
const WebSocket = require('ws');
console.log("WebSocket module loaded successfully.");
// Get server URL from command line argument or use default
const serverUrl = process.argv[2] || 'wss://api.syndica.io/api-key/<YOUR_API_KEY>';
console.log(`Target Server URL: ${serverUrl}`);
console.log("Creating WebSocket connection (perMessageDeflate requested with custom options)...");
// Define ws here so it's accessible in SIGINT even if constructor fails later
// NOTE: permessage-deflate is enabled by default with the following value: `permessage-deflate; client_max_window_bits`
let ws = new WebSocket(serverUrl, {
// perMessageDeflate: false, // disable permessage-deflate if needed
// Instead of just 'true', provide an object for detailed configuration
perMessageDeflate: {
// Request that the client doesn't use context takeover.
// Corresponds to the 'client_no_context_takeover' parameter.
clientNoContextTakeover: true,
// Request that the server doesn't use context takeover.
// Corresponds to the 'server_no_context_takeover' parameter.
serverNoContextTakeover: true, // Example: Request server no context takeover
// Request a specific max window bits for the client.
// Corresponds to the 'client_max_window_bits' parameter.
clientMaxWindowBits: 10, // Example: Request window bits 10 (2^10 = 1024 bytes)
// Request a specific max window bits for the server.
// Corresponds to the 'server_max_window_bits' parameter.
serverMaxWindowBits: 10, // Example
// You can also configure zlib options directly if needed
zlibDeflateOptions: {
// Example: Set a specific compression level (0-9)
// level: 9
},
zlibInflateOptions: {
// windowBits: 15 // Example
},
// Threshold below which messages are not compressed (bytes)
// threshold: 1024, // Example
// Concurrency limit for compression/decompression operations
// concurrencyLimit: 10, // Example
}
});
console.log("WebSocket object created.");
ws.on("upgrade", function upgrade(response) {
// The 'upgrade' event on the client provides the IncomingMessage response
console.log(`UPGRADE response headers: ${JSON.stringify(response.headers)}`);
// Note: ws.extensions might not be populated yet in the 'upgrade' event handler
// It's reliably populated in the 'open' event handler.
});
ws.on('open', function open() {
console.log('WebSocket connection opened.');
// Check negotiated extensions
const negotiatedExtensionsHeader = this.extensions; // This is the raw header string value
console.log(`Negotiated Sec-WebSocket-Extensions header: ${negotiatedExtensionsHeader}`);
if (negotiatedExtensionsHeader && negotiatedExtensionsHeader.includes('permessage-deflate')) {
console.log('permessage-deflate extension negotiated successfully!');
} else {
console.log('Warning: permessage-deflate extension was NOT negotiated.');
}
const initialMessage = JSON.stringify({
"jsonrpc": "2.0",
"id": "TEST",
"method": "solana-mainnet.slotSubscribe"
});
console.log(`Sending: ${initialMessage}`);
ws.send(initialMessage);
});
ws.on('message', function incoming(data) {
// ws library gives Buffer for binary, toString() for text
console.log(`Received: ${data.toString()}`);
});
ws.on('error', function error(err) {
console.error(`WebSocket error: ${err.message}`);
// Optional: Exit on error if preferred
process.exit(1);
});
ws.on('close', function close(code, reason) {
const reasonStr = reason ? reason.toString() : 'No reason given';
console.log(`WebSocket connection closed. Code: ${code}, Reason: ${reasonStr}`);
console.log("--- Node.js Script Ending ---");
});
console.log("WebSocket event listeners attached. Waiting for events...");
// Handle Ctrl+C
process.on('SIGINT', () => {
console.log('\nSIGINT received, closing connection...');
// Check if ws was successfully initialized and is open
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, "Client shutting down via SIGINT");
} else {
console.log("WebSocket not open or not initialized. Exiting immediately.");
process.exit(0); // Exit if already closed or closing
}
});
} catch (error) {
console.error("!!! An error occurred during script setup !!!");
console.error(error);
process.exit(1); // Exit forcefully if setup fails
}