Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Cassandra CQL Protocol Compression

The CQL protocol supports frame-level compression to reduce network bandwidth between clients and Cassandra nodes. Compression is particularly beneficial for large result sets, batch operations, and high-latency network connections.

Network bandwidth is often the bottleneck for Cassandra workloads:

ScenarioUncompressedCompressed (LZ4)Savings
Text-heavy result (1000 rows)500 KB80 KB84%
Binary data result500 KB400 KB20%
Small query/response200 B250 B-25% (overhead)

Compression benefits:

  • Reduced network utilization
  • Lower cloud egress costs
  • Faster transfers on slow links
  • Better throughput on saturated networks

Compression costs:

  • CPU cycles for encode/decode
  • Overhead for small messages
  • Memory for buffers
AlgorithmSpeedRatioUse Case
LZ4Very fastModerateGeneral purpose (recommended)
SnappyFastModerateAlternative to LZ4
NoneN/AN/ALow-latency, small payloads

LZ4 Recommended

LZ4 provides the best balance of compression ratio and CPU efficiency. Enable it by default unless specific requirements dictate otherwise.


Compression is negotiated during connection establishment:

Compression negotiated in the STARTUP message during connection setupClientServerClientClientServerServerSTARTUP {COMPRESSION: "lz4"}alt[Server supports LZ4]READYAll subsequent framesuse LZ4 compression[Server doesn't support]ERROR (Protocol error)
STARTUP {
CQL_VERSION: "3.0.0",
COMPRESSION: "lz4" // or "snappy" or omit for none
}

Query supported compression before connecting:

Client → Server: OPTIONS
Server → Client: SUPPORTED {
COMPRESSION: ["lz4", "snappy"],
CQL_VERSION: ["3.4.6"]
}

The frame header indicates compression:

Frame Header:
Byte 1: flags
Bit 0: COMPRESSION (0x01)
0 = Body is uncompressed
1 = Body is compressed

LZ4 Format:

+-------------------------+
| Frame Header (9 bytes) | ← Not compressed
+-------------------------+
| Uncompressed Length (4) | ← Original size (big-endian int)
+-------------------------+
| LZ4 Compressed Data | ← Compressed frame body
+-------------------------+

Snappy Format:

+-------------------------+
| Frame Header (9 bytes) | ← Not compressed
+-------------------------+
| Snappy Compressed Data | ← Compressed frame body (includes length)
+-------------------------+

Not all frames benefit from compression:

# Conceptual compression decision
def should_compress(body):
# Small bodies may expand with compression
if len(body) < COMPRESSION_THRESHOLD: # e.g., 512 bytes
return False
# Already compressed data doesn't benefit
if is_compressed_content(body):
return False
return True

LZ4 is a fast lossless compression algorithm. The following values are illustrative and vary by hardware and data characteristics:

MetricIllustrative Value
Compression speed~780 MB/s
Decompression speed~4970 MB/s
Compression ratio2.1:1 (typical text)
Memory usageLow

Cassandra uses raw LZ4 block format with explicit length:

LZ4 Compressed Body:
[4 bytes] - Uncompressed length (big-endian)
[N bytes] - LZ4 compressed data
Decompression:
1. Read uncompressed length
2. Allocate output buffer
3. LZ4 decompress into buffer
# Conceptual LZ4 compression
import lz4.block
def compress_frame(body):
compressed = lz4.block.compress(body, store_size=False)
# Prepend original length
length_prefix = len(body).to_bytes(4, 'big')
return length_prefix + compressed
def decompress_frame(data):
uncompressed_length = int.from_bytes(data[:4], 'big')
compressed = data[4:]
return lz4.block.decompress(compressed, uncompressed_size=uncompressed_length)

Snappy prioritizes speed over compression ratio:

MetricValue
Compression speed~500 MB/s
Decompression speed~1500 MB/s
Compression ratio1.5-2:1 (typical)
Memory usageLow

Snappy includes length in its format:

Snappy Compressed Body:
[Varint] - Uncompressed length (Snappy format)
[N bytes] - Snappy compressed data
The length is encoded in the Snappy stream itself.

Data TypeLZ4 RatioSnappy Ratio
ASCII text3:1 - 5:12:1 - 4:1
JSON4:1 - 8:13:1 - 6:1
UUIDs2:1 - 3:11.5:1 - 2:1
Random binary1:1 - 1.1:11:1 - 1.05:1
Repeated values10:1+8:1+
Without compression:
Network time = data_size / bandwidth
CPU time = 0
With compression:
Network time = compressed_size / bandwidth
CPU time = compression_time + decompression_time
Break-even when:
(data_size - compressed_size) / bandwidth > cpu_time

Example calculation:

Data: 100 KB, compresses to 20 KB
Bandwidth: 1 Gbps = 125 MB/s
LZ4 speed: 780 MB/s compress, 4970 MB/s decompress
Without compression:
Network: 100 KB / 125 MB/s = 0.8 ms
With compression:
Compress: 100 KB / 780 MB/s = 0.13 ms
Network: 20 KB / 125 MB/s = 0.16 ms
Decompress: 100 KB / 4970 MB/s = 0.02 ms
Total: 0.31 ms
Savings: 0.8 - 0.31 = 0.49 ms (61% faster)
ScenarioCompression Benefit
Large text resultsHigh
High-latency linksHigh
Bandwidth-limitedHigh
Small queries/responsesLow or negative
Already compressed dataNegative
CPU-bound workloadsLow
  • Very small messages (<512 bytes)
  • Binary/compressed content (images, encrypted data)
  • Extremely CPU-constrained clients
  • Ultra-low-latency requirements (<1ms)

Most drivers default to LZ4 when available:

# Python driver
from cassandra.cluster import Cluster
cluster = Cluster(
compression=True # Uses LZ4 by default
)
# Or explicitly
cluster = Cluster(
compression='lz4' # or 'snappy' or False
)

Cassandra supports compression without additional configuration. The server advertises supported algorithms in the SUPPORTED response.

Some drivers support minimum size thresholds:

// Conceptual - compress only if body > threshold
builder.withCompression(
ProtocolOptions.Compression.LZ4,
512 // Minimum bytes to compress
);

Compression requires temporary buffers:

Compression:
Input buffer: Original data
Output buffer: Max compressed size
Working memory: Algorithm-specific
LZ4 working memory: ~16 KB
Snappy working memory: ~32 KB

Efficient implementations pool buffers:

# Conceptual buffer pool
class CompressionBufferPool:
def __init__(self, max_size, buffer_size):
self.pool = Queue(max_size)
self.buffer_size = buffer_size
def acquire(self):
try:
return self.pool.get_nowait()
except Empty:
return bytearray(self.buffer_size)
def release(self, buffer):
try:
self.pool.put_nowait(buffer)
except Full:
pass # Discard if pool full

For very large frames, streaming reduces memory:

Stream compression:
Process data in chunks
Never hold entire message in memory
Slightly lower compression ratio

MetricPurpose
Compression ratioEffectiveness
Compression timeCPU overhead
Bytes savedBandwidth reduction
Compressed message countUsage patterns
# Conceptual metrics collection
class CompressionMetrics:
def record_compression(self, original_size, compressed_size, duration):
self.ratio_histogram.observe(original_size / compressed_size)
self.time_histogram.observe(duration)
self.bytes_saved.inc(original_size - compressed_size)
self.messages_compressed.inc()

When both compression and TLS are enabled:

Order of operations:
Send: Serialize → Compress → Encrypt → Transmit
Receive: Receive → Decrypt → Decompress → Deserialize
Note: Compression happens BEFORE encryption

Compression before encryption can leak information (CRIME/BREACH attacks):

  • CQL protocol is less vulnerable than HTTP
  • No user-controlled input in headers
  • Still, be aware of theoretical risks
  • Consider disabling compression for highly sensitive data

Compression Side-Channel Attacks

The CRIME/BREACH class of attacks exploit compression to infer encrypted content. While the CQL protocol is less susceptible than HTTP, extremely sensitive deployments should evaluate this risk.


  1. Enable LZ4 by default - Best balance of speed and ratio
  2. Monitor compression metrics - Verify actual benefit
  3. Set appropriate thresholds - Avoid compressing small messages
  4. Profile CPU impact - Ensure not CPU-bound
WorkloadRecommendation
OLTP (small reads/writes)Maybe disable or high threshold
Analytics (large scans)Enable with LZ4
Batch loadingEnable, monitor throughput
Cross-DC replicationEnable, significant savings