Skip to content

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

Cassandra Architecture Asynchronous Connections

Cassandra drivers use asynchronous, non-blocking I/O to achieve high throughput with minimal resources. This architecture enables thousands of concurrent requests using a small number of connections.


Traditional relational databases (PostgreSQL, MySQL, Oracle, SQL Server) use a synchronous, thread-per-connection model inherited from RPC (Remote Procedure Call) patterns:

Traditional RDBMS: Thread-per-Connection ModelTraditional RDBMS: Thread-per-Connection ModelApplication ServerConnection PoolDatabase ServerThread 1(blocked)Thread 2(blocked)Thread 3(blocked)Thread 4(blocked)...(500+ threads)Conn 1Conn 2Conn 3Conn 4...Worker 1Worker 2Worker 3Worker 4Each thread blocked waitingfor database response~1MB stack per threadServer spawns workerprocess/thread per connection

In this model:

  1. One thread per request: Each database query requires a dedicated application thread
  2. Thread blocks until response: The thread cannot do other work while waiting
  3. One connection per active thread: The connection is exclusively held during query execution
  4. Server-side resources: Database creates a worker process/thread for each connection

Resource Exhaustion:

ResourceConsumptionImpact
Application threads1 per concurrent query500 queries = 500 threads = 500MB+ memory
TCP connections1 per threadFile descriptor limits, server connection limits
Database workers1 per connectionmax_connections parameter limits concurrency
Context switches2 per request (app + DB)CPU overhead scales with concurrency

Connection Pool Challenges:

Pool size too small:
- Threads block waiting for connections
- Request queuing and timeouts
- Underutilized server capacity
Pool size too large:
- Memory wasted on idle connections
- Database overwhelmed during bursts
- Connection limits exhausted

Stale Connection Problem:

Traditional connection pools suffer from stale connections:

Stale Connection DetectionStale Connection DetectionApplication requests connectionConnection in pool?yesno"SELECT 1" or similarAdds latency to every requestSend validation queryValidation succeeds?yesnoUse connectionDiscard connectionCreate new connectionunknownConnection still valid?TCP handshakeTLS negotiationAuthentication~10-50ms overheadCreate new connectionExecute queryReturn connection to pool

Stale connections occur when:

  • Network interruption: Firewall drops idle connection, load balancer timeout
  • Server restart: Database recycled but client unaware
  • Idle timeout: Connection exceeds server's wait_timeout
  • TCP keepalive failure: Underlying socket becomes invalid

Connection Recycling Overhead:

OperationTypical LatencyNotes
TCP handshake0.5-1ms (same DC)3-way handshake
TLS negotiation2-5msCertificate exchange, key derivation
Authentication1-3msCredential validation
Session setup1-2msSet timezone, charset, schema
Total5-15msPer new connection

Scaling Limitations:

Traditional Scaling:
1000 requests/sec @ 10ms latency = 10 connections needed
10000 requests/sec @ 10ms latency = 100 connections needed
100000 requests/sec @ 10ms latency = 1000 connections needed ← hits limits
Database limits:
PostgreSQL: max_connections default 100
MySQL: max_connections default 151
Oracle: processes parameter limits
Application limits:
Thread pool sizing
Memory for thread stacks
Context switch overhead

Connection Explosion Under Load

During traffic spikes, applications often exhaust connection pools. New connection creation adds latency, and if the database hits max_connections, requests fail entirely. This creates a cascading failure pattern where increased load leads to decreased capacity.


Cassandra drivers use a fundamentally different approach: multiplexed asynchronous connections with stream IDs:

Cassandra: Stream Multiplexing ModelCassandra: Stream Multiplexing ModelApplication ServerMultiplexed ConnectionCassandra NodeEvent Loop(1-2 threads)Request QueueStream 1Stream 2Stream 3...Stream 32768Native Transport(shared thread pool)Single connection handlesthousands of concurrent requestsNo thread-per-request32,768 streams per connectionRequests identified by stream IDOut-of-order responses

Key Differences (values are illustrative and driver/configuration-dependent):

AspectTraditional RDBMSCassandra
Connection utilization1 query at a time32,768 concurrent queries
Threads required1 per queryMinimal (event-loop based)
Connection count100s needed2-8 per node sufficient
Response orderingSequentialMultiplexed (any order)
Memory per connection~1MB (thread)Much smaller (buffer-based)
New connection costPaid frequentlyPaid rarely (persistent)
  1. No server-side session state: Unlike RDBMS, Cassandra connections have no transaction context, cursors, or prepared statement binding that requires connection affinity

  2. Protocol-level multiplexing: The CQL binary protocol includes stream IDs (0-32767), allowing response matching without connection-per-request

  3. Stateless request handling: Each request contains all necessary context (consistency level, timestamp, etc.), enabling any server thread to handle any request

  4. Designed for distribution: Cassandra expects many nodes, so minimizing per-node connection overhead is essential

Traditional synchronous model:

Thread 1: send request → [blocked waiting 5ms] → receive response → process
Thread 2: send request → [blocked waiting 5ms] → receive response → process
Thread 3: send request → [blocked waiting 5ms] → receive response → process

Cassandra asynchronous model:

Event Loop: send Q1 → send Q2 → send Q3 → send Q4 → send Q5 →
receive R3 → receive R1 → receive R5 → receive R2 → receive R4
AspectSynchronousAsynchronous
Threads per request10 (shared)
Memory per connection~1MB (thread stack)~64KB (buffers)
Context switchesPer requestPer I/O event
Concurrent requestsLimited by threadsLimited by protocol (32K)
Latency overheadThread schedulingMinimal

Connection Efficiency

A single Cassandra connection can handle the same throughput that would require 100+ PostgreSQL connections. For a 9-node cluster, 8 connections per node (72 total) can sustain hundreds of thousands of requests per second.


Driver Connection ComponentsDriver Connection ComponentsApplicationDriverConnectionNetworkApplication CodeRequest QueueResponse HandlerEvent LoopStream ManagerFrame CodecWrite BufferRead BufferTCP Socket1. submit request2. allocate stream3. encode frame10. match stream4. queue write5. flush6. TCP write7. TCP read8. read events9. decode frame11. complete future12. result

The event loop is the core of asynchronous I/O:

  1. Selector/Epoll - Monitors sockets for read/write readiness
  2. Event Dispatch - Routes I/O events to handlers
  3. Task Execution - Runs callbacks and completions

Most drivers use platform-native I/O:

  • Linux: epoll
  • macOS: kqueue
  • Windows: IOCP

Connections use non-blocking TCP sockets:

Socket Configuration:
- TCP_NODELAY: Disable Nagle's algorithm (reduce latency)
- SO_KEEPALIVE: Detect dead connections
- Non-blocking mode: Never block on read/write

The CQL protocol supports up to 32,768 concurrent streams per connection (protocol v3+). Stream allocation:

Stream identifier allocation and release on a CQL connectionApplicationStream ManagerConnectionServerApplicationApplicationStream ManagerStream ManagerConnectionConnectionServerServerallocate streamstream ID 42send(stream=42, query)frame(stream=42)frame(stream=42)response(stream=42)complete futurerelease stream 42

Sequential Allocation:

Streams: 1, 2, 3, 4, ... (wrap at max)
Pros: Simple, predictable
Cons: Head-of-line if stream stuck

Pooled Allocation:

Free list: [5, 12, 7, 23, ...]
Allocate: pop from list
Release: push to list
Pros: Fast allocation, no fragmentation

Drivers limit concurrent requests per connection. Values vary by driver and version:

ConfigurationTypical Value (Java Driver 4.x)Purpose
Max requests per connection1024-2048Prevent overload
High watermark80% of maxTrigger backpressure
Low watermark50% of maxResume after backpressure

Backpressure Behavior

When the high watermark is reached, new requests are queued or rejected until in-flight requests drop below the low watermark. This prevents overwhelming individual connections.


Each node maintains a connection pool:

Connection pools for two nodes with least-loaded routingConnection pools for two nodes with least-loaded routingConnection Pool (Node A)Connection Pool (Node B)Connection 1847 in-flightConnection 2923 in-flightConnection 3156 in-flightConnection 4412 in-flightConnection 5678 in-flightRequest Routerroute (least loaded)route (least loaded)
ParameterDescriptionDefault
Core connectionsMinimum maintained connections1
Max connectionsMaximum connections to create8 (local), 2 (remote)
Max requests/connectionConcurrent requests limit1024

Remote Datacenter Connections

Remote datacenter pools typically use fewer connections (1-2) since cross-DC requests should be rare in normal operation. This conserves resources while maintaining fallback capability.

When routing a request to a pool:

  1. Least-loaded selection - Choose connection with fewest in-flight requests
  2. Round-robin - Cycle through connections (simpler)
  3. Random - Random selection (load spreads naturally)

Pools grow and shrink based on demand:

Scale Up:

if (all_connections_at_max_requests && pool_size < max_connections):
create_new_connection()

Scale Down:

if (connection_idle_time > threshold && pool_size > core_connections):
close_idle_connection()

# Conceptual flow (not actual API)
async def execute(query):
# 1. Select node via load balancer
node = load_balancer.select(query)
# 2. Get connection from pool
connection = await pool.acquire(node)
# 3. Allocate stream
stream_id = connection.allocate_stream()
# 4. Create response future
future = Future()
connection.register(stream_id, future)
# 5. Encode and send
frame = encode_query(stream_id, query)
connection.write(frame)
# 6. Return future (completes when response arrives)
return future
# Conceptual flow
def on_data_received(connection, data):
# 1. Decode frame
frame = decode_frame(data)
# 2. Find waiting future
future = connection.get_pending(frame.stream_id)
# 3. Release stream
connection.release_stream(frame.stream_id)
# 4. Complete future
if frame.is_error():
future.set_exception(decode_error(frame))
else:
future.set_result(decode_result(frame))

Multiple requests can be sent before receiving responses:

Time →
Client: [Q1][Q2][Q3][Q4]----------------→
Server: ------[R2][R1][R4][R3]----------→
Without pipelining (would require):
Client: [Q1]----[Q2]----[Q3]----[Q4]---→
Server: ----[R1]----[R2]----[R3]----[R4]→

Pipelining benefits:

  • Better network utilization
  • Reduced per-request latency
  • Amortized TCP overhead

When the system is overloaded, drivers apply backpressure:

Request-side backpressure decision flowRequest-side backpressure decision flowSubmit requestin_flight < high_watermark?yesnoSend immediatelyin_flight < max_requests?yesnoQueue requestWait for capacityReject requestThrow exception

If the application cannot process responses fast enough:

  1. Buffer responses - Accumulate in memory (risky)
  2. Pause reading - Stop reading from socket (TCP backpressure)
  3. Drop connection - Last resort
SignalMeaningAction
Queue fullToo many pending requestsSlow down submissions
High in-flightApproaching connection limitConsider more connections
Read buffer fullProcessing too slowIncrease consumer capacity

Ignoring Backpressure

Applications that ignore backpressure signals risk memory exhaustion, request timeouts, and cascading failures. Design applications to handle BusyConnectionException or equivalent errors gracefully.


Drivers send periodic heartbeats to detect dead connections:

Heartbeat interval: 30 seconds (typical)
Mechanism: OPTIONS request or protocol-level ping
Timeout: If no response, mark connection unhealthy
CheckFrequencyFailure Action
Heartbeat30sMark unhealthy, reconnect
Read timeoutPer requestRetry on different connection
Write failureImmediateClose connection
Protocol errorImmediateClose connection

When connections fail:

  1. Mark connection dead - Remove from active pool
  2. Schedule reconnection - Exponential backoff
  3. Notify load balancer - May affect node status
Reconnection schedule:
Attempt 1: immediate
Attempt 2: 1 second
Attempt 3: 2 seconds
Attempt 4: 4 seconds
...
Max delay: 60 seconds

Efficient buffer management is critical for performance:

Read Buffers:

Per-connection read buffer: 64KB typical
Allocation: Pre-allocated or pooled
Growth: Expand for large frames

Write Buffers:

Per-connection write buffer: 64KB typical
Coalescing: Batch small writes
Flushing: On buffer full or explicit flush

Drivers pool frequently allocated objects:

ObjectPooling Benefit
FramesAvoid allocation per request
Byte buffersReduce GC pressure
Futures/PromisesReuse completion objects
Row objectsMinimize allocation during iteration

Total driver memory usage:

Memory = (connections × connection_overhead) +
(in_flight_requests × request_overhead) +
(result_sets × result_overhead)
Example:
50 connections × 128KB = 6.4MB
5000 in-flight × 2KB = 10MB
Result buffers = variable
Total: ~20-50MB typical

Some drivers use a single I/O thread:

Pros: Simple, no synchronization needed
Cons: CPU-bound work blocks I/O
Pattern: Node.js driver

Others use thread pools:

I/O threads: Handle network operations
Worker threads: Execute callbacks
Pros: Better CPU utilization
Cons: Synchronization complexity
Pattern: Java driver

Modern drivers often combine approaches:

I/O threads: 1 per N connections
Callback threads: Configurable pool
User code: Application threads

ComponentTypical RangeNotes
Stream allocation<1 μsLock-free in good implementations
Frame encoding1-10 μsDepends on query complexity
Buffer copy1-5 μsZero-copy when possible
Syscall overhead1-10 μsBatching amortizes
Network latency100 μs - 10 msSame DC vs cross-DC

Maximum throughput depends on:

Max throughput = connections × streams_per_connection × (1 / avg_latency)
Example:
8 connections × 1024 streams × (1 / 0.005s) = 1.6M requests/sec theoretical
Practical limits:
- Server capacity
- Network bandwidth
- Serialization CPU
- GC pauses
TechniqueBenefit
Connection warmingAvoid cold start latency
Request coalescingReduce syscalls
Zero-copy buffersMinimize CPU
Prepared statementsReduce encoding