Skip to content

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

Cassandra Driver Query Throttling

Query throttling controls the rate at which clients send requests to Cassandra, preventing overload conditions that degrade cluster performance. Throttling occurs at multiple levels: client-side rate limiting, driver backpressure, and server-side admission control.

Client ApplicationDriverCassandra NodeApplication Rate LimiterRequest QueueConnection ThrottlerIn-Flight LimiterBackpressure HandlerNative TransportRequest SchedulerThread PoolsRate limitedQueued requestsConnection capacityBackpressure signalsControlled flowAdmission controlScheduled execution
LevelPurposeMechanism
ApplicationBusiness logic limitsRate limiters, quotas
DriverPrevent connection overloadIn-flight limits, backpressure
ServerProtect node resourcesQueue limits, rejection

Application-level rate limiting controls request submission:

# Conceptual - Token bucket rate limiter
class RateLimiter:
def __init__(self, rate, burst):
self.rate = rate # Requests per second
self.burst = burst # Maximum burst size
self.tokens = burst
self.last_update = time.now()
def acquire(self):
# Refill tokens based on elapsed time
now = time.now()
elapsed = now - self.last_update
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
self.last_update = now
if self.tokens >= 1:
self.tokens -= 1
return True # Proceed
return False # Wait or reject
StrategyBehaviorUse Case
Token bucketAllows bursts up to limitGeneral purpose
Leaky bucketConstant rate, no burstsStrict rate control
Sliding windowRate over rolling windowAPI quotas
AdaptiveAdjusts based on responseAuto-tuning

When rate-limited, requests enter a queue:

Queue Strategies:
- Bounded queue: Reject when full
- Unbounded queue: Risk memory exhaustion
- Priority queue: Prefer important requests
- Timeout queue: Drop stale requests

Drivers limit concurrent requests per connection:

Connection Capacity:
max_concurrent_requests_per_connection = 1024 (typical)
Total Capacity:
max_concurrent = connections × max_per_connection
Example:
8 connections × 1024 = 8192 concurrent requests

When capacity is reached:

Submit Requestin_flight < max_requests?yesnoSend Immediatelyqueue_size < max_queue?yesnoQueue RequestWait for CapacityReject RequestThrow BusyConnectionException
ParameterDescriptionTypical Default
max_requests_per_connectionConcurrent requests per connection1024-2048
max_queue_sizeWaiting requests256-1024
acquire_timeoutTime to wait for capacity12 seconds

Advanced drivers adjust limits based on server response:

# Conceptual - Adaptive throttling
class AdaptiveThrottler:
def __init__(self):
self.current_limit = 1000
self.min_limit = 100
self.max_limit = 5000
def on_success(self, latency):
if latency < target_latency:
# Room to increase
self.current_limit = min(
self.current_limit * 1.1,
self.max_limit
)
def on_timeout(self):
# Back off aggressively
self.current_limit = max(
self.current_limit * 0.5,
self.min_limit
)
def on_overloaded(self):
# Server signals overload
self.current_limit = max(
self.current_limit * 0.7,
self.min_limit
)

Cassandra limits concurrent connections and requests:

VersionParameterDefault
4.0native_transport_max_frame_size_in_mb16
4.1+native_transport_max_frame_size16MiB
cassandra.yaml
native_transport_max_threads: 128
native_transport_max_concurrent_connections: -1 # unlimited
native_transport_max_concurrent_connections_per_ip: -1

Memory-Based Backpressure (Cassandra 4.0+)

Section titled “Memory-Based Backpressure (Cassandra 4.0+)”

Cassandra 4.0 introduced memory-based backpressure for the native transport (CASSANDRA-15013). This prevents unbounded memory growth from in-flight requests.

Configuration parameter names (version-specific):

VersionGlobal LimitPer-IP Limit
4.0native_transport_max_concurrent_requests_in_bytesnative_transport_max_concurrent_requests_in_bytes_per_ip
4.1+native_transport_max_request_data_in_flightnative_transport_max_request_data_in_flight_per_ip

Defaults: When unset, defaults are auto-calculated (global: ~1/10 heap, per-IP: ~1/40 heap).

# cassandra.yaml (4.1+ syntax)
native_transport_max_request_data_in_flight: null # auto (1/10 heap)
native_transport_max_request_data_in_flight_per_ip: null # auto (1/40 heap)
# Behavior when limits exceeded
native_transport_throw_on_overload: false # false = TCP backpressure; true = OverloadedException
ApproachDescription
TCP backpressure (throw_on_overload: false)Preferred; automatically throttles clients without requiring exception handling
OverloadedException (throw_on_overload: true)Client receives error immediately; requires retry logic

How TCP backpressure works:

  1. In-flight bytes exceed limit
  2. Cassandra stops reading from client sockets (sets autoread=false)
  3. Kernel socket buffers fill
  4. Client's write() calls block
  5. Client naturally slows down
  6. As requests complete, reading resumes

This approach is preferred because it automatically throttles clients without requiring exception handling logic.

The native transport queues requests when threads are busy:

Request Flow:
1. Request arrives at native transport
2. If thread available, process immediately
3. If no thread, queue up to limit
4. If queue full, reject request

When overloaded, servers may respond with:

ERROR {
code: 0x1001 // Overloaded
message: "Server is overloaded"
}

Client behavior on Overloaded:

  1. Do not retry immediately
  2. Back off exponentially
  3. Try different node
  4. Eventually fail request

Overloaded Responses

Receiving Overloaded errors indicates the cluster is under stress. Continuing to send requests at the same rate will worsen the situation. Implement exponential backoff and consider reducing overall request rate.

Cassandra 5.0 introduces server-side rate limiting:

ERROR {
code: 0x.. // Rate limit error
message: "Rate limit exceeded"
// Additional: retry_after hint
}

Limit total cluster throughput:

# Distributed rate limiter (conceptual)
class ClusterRateLimiter:
def __init__(self, redis_client, rate_limit):
self.redis = redis_client
self.limit = rate_limit
self.window = 1 # second
def acquire(self, key="global"):
current = self.redis.incr(f"rate:{key}")
if current == 1:
self.redis.expire(f"rate:{key}", self.window)
if current > self.limit:
return False
return True

Multi-tenant applications need per-tenant limits:

# Per-tenant throttling
class TenantThrottler:
def __init__(self, limits):
self.limiters = {}
self.default_limit = limits["default"]
def acquire(self, tenant_id):
if tenant_id not in self.limiters:
limit = self.get_tenant_limit(tenant_id)
self.limiters[tenant_id] = RateLimiter(limit)
return self.limiters[tenant_id].acquire()

Different priorities for different operations:

# Priority queues
class PriorityThrottler:
def __init__(self):
self.queues = {
"critical": Queue(maxsize=100), # Always processed
"normal": Queue(maxsize=1000), # Standard priority
"background": Queue(maxsize=5000) # Best effort
}
def submit(self, request, priority="normal"):
queue = self.queues[priority]
if queue.full():
if priority == "critical":
# Evict from lower priority
self.evict("background")
else:
raise ThrottledException()
queue.put(request)

SignalMeaningAction
Success (fast)HealthyMaintain or increase rate
Success (slow)Approaching limitReduce rate slightly
TimeoutOverloadedReduce rate significantly
Overloaded errorServer strugglingBack off, try other node
UnavailableCapacity issueCircuit breaker

Use latency percentiles to tune throttling:

# Latency-based throttling
class LatencyThrottler:
def __init__(self, target_p99):
self.target = target_p99
self.rate = 1000 # Initial rate
def observe(self, latencies):
p99 = percentile(latencies, 99)
if p99 < self.target * 0.8:
# Well under target, increase
self.rate *= 1.1
elif p99 > self.target:
# Over target, decrease
self.rate *= 0.9
return self.rate

Circuit Breaker Pattern

Circuit breakers are a critical resiliency pattern that prevents a failing service from being overwhelmed with requests, allowing it time to recover.

Circuit breakers prevent cascading failures:

ClosedNormal operationOpenFailing fastHalf-OpenTesting recoveryFailure threshold exceededTimeout elapsedTest request failsTest request succeeds
# Conceptual circuit breaker
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.state = "CLOSED"
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.last_failure_time = None
def call(self, operation):
if self.state == "OPEN":
if time.now() - self.last_failure_time > self.recovery_timeout:
self.state = "HALF_OPEN"
else:
raise CircuitOpenException()
try:
result = operation()
self.on_success()
return result
except Exception as e:
self.on_failure()
raise
def on_success(self):
self.failure_count = 0
self.state = "CLOSED"
def on_failure(self):
self.failure_count += 1
self.last_failure_time = time.now()
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"

MetricDescriptionAlert Threshold
Throttled requestsRequests rate limited> 1% of traffic
Queue depthWaiting requests> 80% of max
Rejection rateRequests droppedAny rejections
Backpressure eventsDriver throttlingIncreasing trend
# Monitor throttling state (conceptual)
def throttling_status():
return {
"current_rate": rate_limiter.current_rate,
"queue_depth": request_queue.size(),
"in_flight": connection_pool.in_flight_count(),
"rejections_1m": metrics.get("rejections", window="1m"),
"p99_latency": metrics.get("latency_p99")
}

WorkloadRate LimitQueue SizeTimeout
OLTP10K-100K/sSmall (256)Short (1s)
Analytics1K-10K/sLarge (10K)Long (60s)
Batch100-1K/sMedium (1K)Medium (10s)

Avoid thundering herd on startup:

# Gradual traffic ramp
def ramp_up(target_rate, duration):
steps = 10
step_duration = duration / steps
step_increase = target_rate / steps
current = step_increase
for _ in range(steps):
rate_limiter.set_rate(current)
sleep(step_duration)
current += step_increase

When overloaded, shed less critical load:

# Load shedding strategy
def should_accept(request):
current_load = get_current_load()
if current_load < 0.7:
return True # Accept all
if current_load < 0.9:
# Shed background work
return request.priority != "background"
if current_load < 0.95:
# Keep only critical
return request.priority == "critical"
# Extreme load - reject all new requests
return False