Cassandra Driver Guide¶
This guide covers connecting applications to Apache Cassandra using official drivers, with setup and basic usage for popular programming languages.
How Cassandra Drivers Differ from Traditional Database Drivers¶
Cassandra's native protocol works fundamentally differently from traditional database drivers (JDBC, ODBC, etc.). Understanding these differences is essential for building efficient applications.
Multiplexed Connections, Not Connection Pools¶
Traditional database drivers use a connection-per-query model:
- Each query requires an exclusive connection
- Connection pools manage a fixed set of connections (e.g., 10-100)
- Under load, queries queue waiting for an available connection
- Each connection = one in-flight request
Cassandra drivers use a multiplexed connection model:
- A single TCP connection handles thousands of concurrent requests (up to 32,768 with protocol v3+)
- Requests are tagged with unique stream IDs and multiplexed over the connection
- Responses are matched to requests by stream ID, arriving in any order
- No query queuing at the connection level - all requests are immediately in-flight
Traditional (MySQL, PostgreSQL):
Connection 1: [Query A waiting...] → [Response A]
Connection 2: [Query B waiting...] → [Response B]
Connection 3: [Query C waiting...] → [Response C]
(3 connections for 3 concurrent queries)
Cassandra (multiplexed):
Connection 1: [Query A id=1] [Query B id=2] [Query C id=3] → [Response B] [Response A] [Response C]
(1 connection for thousands of concurrent queries)
This means you do not need large connection pools. A single connection per host is often sufficient, and the driver manages this automatically.
Persistent Connections with Cluster Awareness¶
Unlike traditional drivers that simply execute queries, Cassandra drivers maintain an ongoing relationship with the cluster. This is why Cluster and Session instances are designed to be long-lived - you create them once at application startup and reuse them for the entire application lifecycle. Creating multiple instances is unnecessary and wasteful; a single Session can handle all your application's queries concurrently.
At connection time:
- Driver connects to one of the provided contact points
- Queries system tables to discover all nodes, their tokens, and datacenter/rack topology
- Establishes connections to nodes based on the load balancing policy
- Subscribes to cluster event notifications
During operation:
The driver continuously receives push notifications from the cluster about:
- Topology changes - nodes added, removed, or moving tokens
- Status changes - nodes going up or down (immediate notification, not polling)
- Schema changes - tables created, altered, or dropped
This makes the driver highly available and self-healing:
- If a node goes down, the driver is notified immediately and routes around it
- New nodes are automatically discovered and added to the connection pool
- Schema changes are detected and prepared statements are automatically re-prepared
Dynamic Connection Management¶
The driver automatically adjusts connections based on load and cluster changes:
- Connections are established lazily to nodes as needed
- Failed connections are automatically retried with configurable backoff
- Connections are distributed based on load balancing policy (not all nodes need connections)
- The driver maintains heartbeats to detect stale connections
Request Routing Intelligence¶
Unlike traditional drivers that send all queries to a single endpoint (or load balancer), Cassandra drivers include token-aware routing:
- The driver knows which nodes own which data (via token ranges)
- Queries are sent directly to a node that has the data locally
- This eliminates an extra network hop that a coordinator would add
- The routing table is continuously updated as topology changes
Implications for Application Design¶
| Traditional Driver Pattern | Cassandra Driver Pattern |
|---|---|
| Configure connection pool size carefully | Minimal configuration needed - driver auto-manages |
| Monitor pool exhaustion | Monitor per-host request queues |
| Scale connections with load | Scale with more application instances |
| Health checks via test queries | Health managed via protocol events |
| Retry logic in application | Retry policies built into driver |
| Manual failover configuration | Automatic failover via cluster awareness |
Available Drivers¶
| Language | Driver | Status | Repository |
|---|---|---|---|
| Java | Apache Cassandra Java Driver | Production | GitHub |
| Scala/Java | Apache Spark Cassandra Connector | Production | GitHub |
| Python | Apache Cassandra Python Driver | Production | GitHub |
| Python | Async Python Cassandra Client | Early Release | GitHub |
| Node.js | DataStax Node.js Driver | Production | GitHub |
| Go | GoCQL | Production | GitHub |
| C#/.NET | DataStax C# Driver | Production | GitHub |
| C/C++ | Apache Cassandra C++ Driver | Production | GitHub |
| Ruby | DataStax Ruby Driver | Maintenance | GitHub |
| PHP | DataStax PHP Driver | Maintenance | GitHub |
Quick Start Examples¶
Java¶
<!-- pom.xml -->
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-core</artifactId>
<version>4.17.0</version>
</dependency>
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.cql.*;
public class QuickStart {
public static void main(String[] args) {
try (CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("datacenter1")
.build()) {
ResultSet rs = session.execute("SELECT release_version FROM system.local");
Row row = rs.one();
System.out.println("Cassandra version: " + row.getString("release_version"));
}
}
}
Python¶
pip install cassandra-driver
from cassandra.cluster import Cluster
cluster = Cluster(['127.0.0.1'])
session = cluster.connect()
row = session.execute("SELECT release_version FROM system.local").one()
print(f"Cassandra version: {row.release_version}")
cluster.shutdown()
Python (Async)¶
For async frameworks like FastAPI and aiohttp. Requires Python 3.12+ and Cassandra 4.0+:
pip install async-cassandra
import asyncio
from async_cassandra import AsyncCluster
async def main():
# Create long-lived cluster and session
cluster = AsyncCluster(['127.0.0.1'])
session = await cluster.connect()
result = await session.execute("SELECT release_version FROM system.local")
print(f"Cassandra version: {result.one().release_version}")
# Only close when application shuts down
await session.close()
await cluster.shutdown()
asyncio.run(main())
Node.js¶
npm install cassandra-driver
const cassandra = require('cassandra-driver');
const client = new cassandra.Client({
contactPoints: ['127.0.0.1'],
localDataCenter: 'datacenter1'
});
async function run() {
await client.connect();
const result = await client.execute('SELECT release_version FROM system.local');
console.log('Cassandra version:', result.rows[0].release_version);
await client.shutdown();
}
run();
Go¶
go get github.com/gocql/gocql
package main
import (
"fmt"
"log"
"github.com/gocql/gocql"
)
func main() {
cluster := gocql.NewCluster("127.0.0.1")
cluster.Keyspace = "system"
session, err := cluster.CreateSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
var version string
if err := session.Query("SELECT release_version FROM local").Scan(&version); err != nil {
log.Fatal(err)
}
fmt.Println("Cassandra version:", version)
}
Connection Configuration¶
Essential Settings¶
All drivers require these settings:
| Setting | Description | Example |
|---|---|---|
| Contact Points | Initial nodes to connect to | ["10.0.0.1", "10.0.0.2"] |
| Local Datacenter | Preferred DC for routing | "dc1" |
| Port | CQL native port | 9042 |
| Keyspace | Default keyspace (optional) | "my_app" |
Authentication¶
Java:
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("dc1")
.withAuthCredentials("app_user", "app_password")
.build();
Python:
from cassandra.cluster import Cluster
from cassandra.auth import PlainTextAuthProvider
auth_provider = PlainTextAuthProvider(
username='app_user',
password='app_password'
)
cluster = Cluster(
['127.0.0.1'],
auth_provider=auth_provider
)
SSL/TLS¶
Java (application.conf):
datastax-java-driver {
advanced.ssl-engine-factory {
class = DefaultSslEngineFactory
truststore-path = /path/to/truststore.jks
truststore-password = truststorepass
keystore-path = /path/to/keystore.jks
keystore-password = keystorepass
}
}
// SSL is automatically enabled when ssl-engine-factory is configured
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("dc1")
.build();
Python:
from cassandra.cluster import Cluster
import ssl
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_verify_locations('/path/to/ca.crt')
ssl_context.load_cert_chain(
certfile='/path/to/client.crt',
keyfile='/path/to/client.key'
)
cluster = Cluster(
['127.0.0.1'],
ssl_context=ssl_context
)
Java Driver Configuration System¶
The Java driver uses a unique configuration approach based on the Typesafe Config library. This is different from other drivers which typically use only programmatic configuration.
How It Works¶
The driver ships with a reference.conf file containing sensible defaults for all options. You create an application.conf file to override specific settings - you only need to specify what you want to change, not the entire configuration.
Configuration files use HOCON (Human-Optimized Config Object Notation), an improved JSON superset that supports comments, substitutions, and includes.
application.conf (place in your classpath):
datastax-java-driver {
basic {
contact-points = ["10.0.0.1:9042", "10.0.0.2:9042"]
# Explicit local DC - if omitted, driver uses the DC of the first contact point that responds
load-balancing-policy.local-datacenter = "dc1"
request.timeout = 5 seconds
}
advanced {
connection {
pool.local.size = 4
pool.remote.size = 2
}
retry-policy.class = DefaultRetryPolicy
}
}
// Driver automatically loads application.conf from classpath
CqlSession session = CqlSession.builder().build();
Configuration Loading Order¶
The driver checks these locations in order (later sources override earlier ones):
reference.conf(built-in driver defaults)application.properties(classpath)application.json(classpath)application.conf(classpath)- System properties
Execution Profiles¶
Profiles allow different configuration sets for different query types without changing code:
datastax-java-driver {
basic.request.timeout = 2 seconds
profiles {
oltp {
basic.request.timeout = 100 milliseconds
basic.request.consistency = LOCAL_QUORUM
}
olap {
basic.request.timeout = 30 seconds
basic.request.consistency = LOCAL_ONE
}
}
}
// Use profiles per-query
PreparedStatement stmt = session.prepare("SELECT * FROM large_table");
session.execute(stmt.bind().setExecutionProfileName("olap"));
Programmatic Configuration¶
For frameworks with their own configuration mechanisms (Spring Boot, Quarkus), you can build configuration programmatically:
DriverConfigLoader loader = DriverConfigLoader.programmaticBuilder()
.withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(5))
.withString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, "dc1")
.startProfile("slow")
.withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(30))
.endProfile()
.build();
CqlSession session = CqlSession.builder()
.withConfigLoader(loader)
.build();
Loading from External Files¶
Load configuration from files outside the classpath:
// From filesystem
DriverConfigLoader loader = DriverConfigLoader.fromFile(
new File("/etc/myapp/cassandra.conf"));
// From URL
DriverConfigLoader loader = DriverConfigLoader.fromUrl(
new URL("http://config-server/cassandra.conf"));
CqlSession session = CqlSession.builder()
.withConfigLoader(loader)
.build();
Configuration Reloading¶
By default, configuration files are reloaded periodically and the driver adjusts settings dynamically:
datastax-java-driver {
basic.config-reload-interval = 5 minutes
}
Java Driver Only
This file-based configuration system is unique to the Java driver. Python, Node.js, Go, and other drivers use programmatic configuration only. For comprehensive details, see the Java Driver Configuration Reference.
Load Balancing Policies¶
Load balancing policies determine which Cassandra node receives each query. The right policy reduces latency, distributes load evenly, and ensures queries stay within the correct datacenter for multi-DC deployments.
Understanding Contact Points¶
The contact points you provide when creating a cluster connection are not a list of nodes for failover - they are bootstrap points used only for initial cluster discovery.
When the driver connects:
- It connects to one of the contact points
- It queries the system tables to discover all nodes in the cluster
- It establishes connections to other nodes based on the load balancing policy
- It subscribes to cluster events via the native protocol
After bootstrap, the driver continuously receives updates about:
- Topology changes - nodes joining, leaving, or moving tokens
- Node status - nodes going up or down
- Schema changes - new tables, altered columns, dropped keyspaces
This means even if all your contact points go down after initial connection, the driver remains connected and aware of the cluster state through its existing connections. The contact points are only needed again if the driver completely disconnects and needs to re-bootstrap.
Java:
// Contact points are ONLY used for initial bootstrap - not ongoing connections
// When using multi-DC contact points, set local DC explicitly to ensure deterministic routing
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("node1.dc1.example.com", 9042))
.addContactPoint(new InetSocketAddress("node1.dc2.example.com", 9042)) // DC2 - bootstrap fallback
.withLocalDatacenter("dc1") // Explicit DC; if omitted, uses DC of first responding contact point
.build();
Python:
from cassandra.cluster import Cluster
from cassandra.policies import DCAwareRoundRobinPolicy
# Contact points are ONLY used for initial bootstrap - not ongoing connections
# If all contact points are in the same DC, the driver auto-detects local DC
cluster = Cluster(['node1.dc1.example.com', 'node2.dc1.example.com'])
# When using multi-DC contact points, set local DC explicitly to ensure deterministic routing
cluster = Cluster(
contact_points=[
'node1.dc1.example.com', # DC1
'node1.dc2.example.com', # DC2 - in case DC1 is unreachable at startup
],
load_balancing_policy=DCAwareRoundRobinPolicy(local_dc='dc1')
)
Why Load Balancing Matters¶
In a Cassandra cluster, data is distributed across nodes using consistent hashing. Each partition key hashes to a specific token, and that token determines which nodes store the data (based on replication factor). A good load balancing policy:
- Reduces latency by sending queries directly to nodes that own the data
- Avoids cross-DC traffic by preferring local nodes in multi-DC deployments
- Distributes load evenly to prevent hotspots
- Handles failures gracefully by trying other nodes when one is unavailable
Policy Types¶
| Policy | Description | Use Case |
|---|---|---|
| Token-Aware | Routes to the node owning the partition | Default choice - lowest latency |
| DC-Aware Round Robin | Prefers nodes in the local DC, round-robins among them | Multi-DC deployments |
| Round Robin | Distributes evenly across all nodes | Rarely used - ignores data locality |
| Whitelist/Allowlist | Restricts to specific nodes | Testing, maintenance |
Token-Aware Policy (Recommended)¶
Token-aware routing sends queries directly to a node that owns the requested partition, eliminating an extra network hop. Without token-awareness, a coordinator node must forward the query to the replica nodes, adding latency.
Without Token-Aware:
Client → Coordinator → Replica (owns data) → Coordinator → Client
With Token-Aware:
Client → Replica (owns data) → Client
Java (token-aware is default in driver v4+):
// Token-aware + DC-aware is the default behavior
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("dc1") // Explicit DC; if omitted, uses DC of first responding contact point
.build();
Python (sync driver):
from cassandra.cluster import Cluster
from cassandra.policies import TokenAwarePolicy, DCAwareRoundRobinPolicy
# Wrap DC-aware policy with token-aware for best performance
cluster = Cluster(
['127.0.0.1'],
load_balancing_policy=TokenAwarePolicy(
DCAwareRoundRobinPolicy(local_dc='dc1')
)
)
Python (async driver):
from async_cassandra import AsyncCluster
from cassandra.policies import TokenAwarePolicy, DCAwareRoundRobinPolicy
cluster = AsyncCluster(
['127.0.0.1'],
load_balancing_policy=TokenAwarePolicy(
DCAwareRoundRobinPolicy(local_dc='dc1')
)
)
session = await cluster.connect('my_keyspace')
Node.js:
const cassandra = require('cassandra-driver');
const client = new cassandra.Client({
contactPoints: ['127.0.0.1'],
localDataCenter: 'dc1',
// Token-aware is enabled by default in Node.js driver
});
DC-Aware Round Robin¶
In multi-datacenter deployments, DC-aware routing ensures queries go to nodes in the local datacenter, avoiding cross-DC latency (which can be 10-100x higher than local).
Java:
// Local DC is required in Java driver v4+
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("dc1") // Queries only go to dc1 nodes
.build();
Python:
from cassandra.cluster import Cluster
from cassandra.policies import DCAwareRoundRobinPolicy
cluster = Cluster(
['127.0.0.1'],
load_balancing_policy=DCAwareRoundRobinPolicy(
local_dc='dc1',
used_hosts_per_remote_dc=0 # Never use remote DC nodes (default)
)
)
Node.js:
const client = new cassandra.Client({
contactPoints: ['127.0.0.1'],
localDataCenter: 'dc1', // Explicit DC; if omitted, uses DC of first responding contact point
});
Automatic Local Datacenter Detection
If you don't explicitly set the local datacenter, the driver uses the datacenter of the first contact point that responds. This means you can control the local DC implicitly by only providing contact points from a single DC. However, if your contact points span multiple DCs, the driver's local DC becomes non-deterministic - whichever node responds first determines routing for the lifetime of the session.
Configuring Failover to Remote DCs¶
By default, drivers only use local DC nodes. To allow failover to remote DCs when all local nodes are down:
Python:
from cassandra.policies import DCAwareRoundRobinPolicy, TokenAwarePolicy
cluster = Cluster(
['127.0.0.1'],
load_balancing_policy=TokenAwarePolicy(
DCAwareRoundRobinPolicy(
local_dc='dc1',
used_hosts_per_remote_dc=2 # Use up to 2 nodes per remote DC as fallback
)
)
)
Java (application.conf):
datastax-java-driver {
basic.load-balancing-policy {
local-datacenter = "dc1"
}
advanced.load-balancing-policy {
dc-failover {
max-nodes-per-remote-dc = 2
allow-for-local-consistency-levels = false
}
}
}
Avoiding Common Mistakes¶
| Mistake | Problem | Solution |
|---|---|---|
| Multi-DC contact points without explicit local DC | Non-deterministic DC selection based on first responder | Set explicit local_dc or use contact points from single DC only |
| Using Round Robin | Ignores data locality, higher latency | Use Token-Aware + DC-Aware |
| Hardcoding contact points from one DC | If that DC is down, cannot bootstrap | Include contact points from multiple DCs (with explicit local DC) |
| Allowing remote DC for LOCAL_* consistency | Violates consistency guarantees | Set allow-for-local-consistency-levels = false |
Prepared Statements¶
Prepared statements are the correct way to execute parameterized queries in Cassandra. They provide significant benefits over simple string-based queries.
Why Use Prepared Statements?¶
Performance: When you prepare a statement, Cassandra parses the CQL, validates it against the schema, and creates an execution plan. This happens once. Subsequent executions skip all parsing and planning - only the bound values are sent to the server. For queries executed thousands of times, this dramatically reduces CPU usage on both client and server.
Security: Prepared statements prevent CQL injection attacks. Values are bound as typed parameters, not concatenated into the query string, so malicious input cannot alter the query structure.
Type Safety: The driver validates that bound values match the expected types before sending to the server, catching type errors early rather than at execution time.
Token-Aware Routing: Prepared statements include partition key metadata, enabling the driver to route queries directly to the node that owns the data - impossible with unparsed query strings.
Correct Usage Pattern¶
Prepare statements once at application startup or first use, then reuse the prepared statement for all executions:
# Python - Prepare once, execute many times
from cassandra.cluster import Cluster
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('my_keyspace')
# Prepare once (typically at startup or in an initialization block)
insert_stmt = session.prepare("""
INSERT INTO users (user_id, username, email)
VALUES (?, ?, ?)
""")
# Execute many times with different values
import uuid
session.execute(insert_stmt, [uuid.uuid4(), 'john_doe', '[email protected]'])
session.execute(insert_stmt, [uuid.uuid4(), 'jane_doe', '[email protected]'])
// Java - Prepare once, execute many times
PreparedStatement insertStmt = session.prepare(
"INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)"
);
// Execute many times
BoundStatement bound = insertStmt.bind(UUID.randomUUID(), "john_doe", "[email protected]");
session.execute(bound);
# Python (async) - Prepare once, execute many times
from async_cassandra import AsyncCluster
cluster = AsyncCluster(['127.0.0.1'])
session = await cluster.connect('my_keyspace')
# Prepare once
insert_stmt = await session.prepare(
"INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)"
)
# Execute many times
await session.execute(insert_stmt, [user_id, 'john_doe', '[email protected]'])
Anti-Patterns to Avoid¶
❌ Preparing the Same Statement Repeatedly¶
Java:
// WRONG - Prepares on every call, wasting resources
public void insertUser(CqlSession session, UUID userId, String username, String email) {
PreparedStatement stmt = session.prepare(
"INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)");
session.execute(stmt.bind(userId, username, email));
}
// RIGHT - Prepare once at startup, reuse
private final PreparedStatement insertStmt;
public UserRepository(CqlSession session) {
this.insertStmt = session.prepare(
"INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)");
}
public void insertUser(UUID userId, String username, String email) {
session.execute(insertStmt.bind(userId, username, email));
}
Python:
# WRONG - Prepares on every call, wasting resources
def insert_user(session, user_id, username, email):
stmt = session.prepare("INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)")
session.execute(stmt, [user_id, username, email])
# RIGHT - Prepare once, reuse
insert_stmt = session.prepare("INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)")
def insert_user(session, user_id, username, email):
session.execute(insert_stmt, [user_id, username, email])
Each prepare() call sends a request to the server. Preparing the same statement repeatedly wastes network round-trips and server CPU.
❌ Embedding Values in the Query String¶
Java:
// WRONG - Values in query string (CQL injection risk, no caching benefit)
String username = "john_doe";
session.execute("SELECT * FROM users WHERE username = '" + username + "'");
// WRONG - Still wrong, even with prepare
session.prepare("SELECT * FROM users WHERE username = '" + username + "'");
// RIGHT - Use bind parameters
PreparedStatement stmt = session.prepare("SELECT * FROM users WHERE username = ?");
session.execute(stmt.bind(username));
Python:
# WRONG - Values in query string (CQL injection risk, no caching benefit)
username = "john_doe"
session.execute(f"SELECT * FROM users WHERE username = '{username}'")
# WRONG - Still wrong, even with prepare
session.prepare(f"SELECT * FROM users WHERE username = '{username}'")
# RIGHT - Use bind parameters
stmt = session.prepare("SELECT * FROM users WHERE username = ?")
session.execute(stmt, [username])
Embedding values in the query string: - Creates a unique query for each value, defeating caching - Opens CQL injection vulnerabilities - Prevents token-aware routing
❌ Dynamic Table or Column Names¶
Java:
// WRONG - Cannot use bind parameters for table/column names
String table = "users";
PreparedStatement stmt = session.prepare(
"SELECT * FROM " + table + " WHERE id = ?"); // Creates new prepared stmt per table
// Acceptable only if limited set of tables - prepare each once at startup
private final PreparedStatement userStmt;
private final PreparedStatement orderStmt;
public Repository(CqlSession session) {
this.userStmt = session.prepare("SELECT * FROM users WHERE id = ?");
this.orderStmt = session.prepare("SELECT * FROM orders WHERE id = ?");
}
Python:
# WRONG - Cannot use bind parameters for table/column names
table = "users"
stmt = session.prepare(f"SELECT * FROM {table} WHERE id = ?") # Creates new prepared stmt per table
# Acceptable only if limited set of tables - prepare each once at startup
user_stmt = session.prepare("SELECT * FROM users WHERE id = ?")
order_stmt = session.prepare("SELECT * FROM orders WHERE id = ?")
Bind parameters (?) can only be used for values, not for table names, column names, or other CQL syntax. If you need dynamic table access, prepare all variants at startup.
Server-Side Prepared Statement Cache¶
Cassandra caches prepared statements on each node. You can monitor this cache to detect issues:
Key metrics to monitor:
| Metric | Description | Warning Sign |
|---|---|---|
PreparedStatementsCount |
Number of cached prepared statements | Continuously growing = prepare leak |
PreparedStatementsEvicted |
Statements evicted from cache | High rate = cache too small or over-preparing |
PreparedStatementsExecuted |
Execution count | Should be >> PreparedStatementsCount |
Using nodetool:
# View prepared statement cache stats
nodetool info | grep -i prepared
Cassandra configuration (cassandra.yaml):
# Maximum number of prepared statements to cache (default: 10000)
prepared_statements_cache_size_mb: 100
Prepared Statement Leak
If PreparedStatementsCount grows continuously without bound, you likely have a prepare leak - code that prepares statements with dynamic values embedded in the query string. Each unique query string creates a new cache entry until the cache fills and evictions begin, degrading performance.
Consistency Levels¶
Set consistency per query:
# Python
from cassandra import ConsistencyLevel
from cassandra.query import SimpleStatement
stmt = SimpleStatement(
"SELECT * FROM users WHERE user_id = %s",
consistency_level=ConsistencyLevel.QUORUM
)
session.execute(stmt, [user_id])
// Java
session.execute(
SimpleStatement.newInstance("SELECT * FROM users WHERE user_id = ?", userId)
.setConsistencyLevel(DefaultConsistencyLevel.QUORUM)
);
Consistency Level Reference¶
| Level | Reads | Writes | Use Case |
|---|---|---|---|
ONE |
Fast | Fast | Non-critical data |
QUORUM |
Majority | Majority | Default for most apps |
LOCAL_QUORUM |
Local majority | Local majority | Multi-DC deployments |
ALL |
All replicas | All replicas | Highest consistency |
Async Operations¶
Asynchronous database operations are essential for building high-throughput, responsive applications. Rather than blocking a thread while waiting for Cassandra to respond, async operations allow your application to handle other work concurrently.
Why Async Matters¶
Python and the GIL: Python's Global Interpreter Lock (GIL) means only one thread can execute Python bytecode at a time. In synchronous code, when your application waits for a Cassandra query to return, that thread is blocked and cannot process other requests. With async/await, the event loop can handle thousands of concurrent requests on a single thread by switching between tasks during I/O waits. This is particularly critical for web frameworks like FastAPI and aiohttp where blocking the event loop freezes your entire application.
Java and Thread Efficiency: While Java does not have a GIL, creating threads is expensive (typically 1MB of stack memory each). Synchronous drivers require one thread per concurrent query, limiting scalability. Async operations with CompletionStage allow a small thread pool to handle many concurrent requests, reducing memory overhead and context-switching costs. This is especially valuable in reactive frameworks like Spring WebFlux and Vert.x.
Node.js: Being single-threaded and event-driven by design, Node.js naturally benefits from async operations. The driver's Promise-based API integrates seamlessly with the event loop.
Python (asyncio)¶
Use the Async Python Cassandra Client for true async/await support with frameworks like FastAPI and aiohttp:
import asyncio
import uuid
from async_cassandra import AsyncCluster
# Long-lived cluster and session (create once, reuse for all requests)
cluster = None
session = None
async def startup():
global cluster, session
cluster = AsyncCluster(['127.0.0.1'])
session = await cluster.connect('my_keyspace')
async def shutdown():
if session:
await session.close()
if cluster:
await cluster.shutdown()
async def insert_users():
# Prepare statement once, execute many times
insert_stmt = await session.prepare(
"INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)"
)
# Run multiple inserts concurrently
tasks = [
session.execute(insert_stmt, [uuid.uuid4(), f'user{i}', f'user{i}@example.com'])
for i in range(100)
]
await asyncio.gather(*tasks)
async def main():
await startup()
try:
await insert_users()
finally:
await shutdown()
asyncio.run(main())
For streaming large result sets without memory exhaustion, use execute_stream() with a context manager:
from async_cassandra.streaming import StreamConfig
config = StreamConfig(fetch_size=1000)
# Context manager ensures streaming resources are cleaned up
async with await session.execute_stream(
"SELECT * FROM large_table",
stream_config=config
) as result:
async for row in result:
await process_row(row) # Non-blocking, other requests keep flowing
Java (CompletionStage)¶
CompletionStage<AsyncResultSet> future = session.executeAsync(
"SELECT * FROM users WHERE user_id = ?", userId
);
future.thenAccept(resultSet -> {
Row row = resultSet.one();
System.out.println("Username: " + row.getString("username"));
});
Node.js (Promise-based)¶
// Execute multiple queries concurrently
const queries = [
client.execute('SELECT * FROM users WHERE user_id = ?', [userId1]),
client.execute('SELECT * FROM users WHERE user_id = ?', [userId2]),
client.execute('SELECT * FROM users WHERE user_id = ?', [userId3])
];
const results = await Promise.all(queries);
Connection Pooling¶
Drivers maintain connection pools automatically. Key settings:
Python (sync driver)¶
from cassandra.cluster import Cluster
from cassandra.policies import HostDistance
cluster = Cluster(['127.0.0.1'])
# Set pool size per host (only works with protocol v1/v2)
cluster.set_core_connections_per_host(HostDistance.LOCAL, 4)
cluster.set_max_connections_per_host(HostDistance.LOCAL, 10)
Python (async driver)¶
With protocol v3+ (required by async-cassandra), the Python driver uses a single connection per host that supports up to 32,768 concurrent requests. This is more efficient than multiple connections due to reduced lock contention and overhead.
from async_cassandra import AsyncCluster
cluster = AsyncCluster(
['127.0.0.1'],
executor_threads=4, # Thread pool for callbacks (default: 2)
idle_heartbeat_interval=30, # Keep-alive interval in seconds
connect_timeout=10, # Connection timeout (default: 5)
request_timeout=10, # Per-request timeout (default: 10)
)
Java¶
// application.conf
datastax-java-driver {
advanced.connection {
pool {
local.size = 4
remote.size = 2
}
}
}
Retry Policies¶
Retry policies determine how drivers handle transient failures such as timeouts, temporary node unavailability, or network issues. When a query fails, the retry policy decides whether to:
- Retry the query on the same or a different node
- Rethrow the exception to the application
- Ignore the failure (for writes where partial success is acceptable)
Idempotency: Critical for Safe Retries¶
Drivers only retry queries that are marked as idempotent. A query is idempotent if executing it multiple times produces the same result as executing it once. This is essential because when a timeout occurs, the driver cannot know whether the query actually succeeded on the server.
| Query Type | Idempotent? | Reason |
|---|---|---|
SELECT * FROM users WHERE id = ? |
✅ Yes | Same data returned each time |
UPDATE users SET name = 'John' WHERE id = ? |
✅ Yes | Setting to absolute value is repeatable |
DELETE FROM users WHERE id = ? |
✅ Yes | Deleting twice has same effect |
INSERT INTO users (id, name) VALUES (?, ?) IF NOT EXISTS |
✅ Yes | LWT ensures exactly-once semantics |
SELECT now() FROM system.local |
❌ No | Returns different timestamp each call |
UPDATE users SET counter = counter + 1 WHERE id = ? |
❌ No | Increment applied multiple times |
INSERT INTO logs (id, ts) VALUES (uuid(), ?) |
❌ No | Generates different UUID each time |
Default: Queries are NOT idempotent
By default, drivers assume queries are not idempotent and will not retry them on timeout. You MUST explicitly mark queries as idempotent to enable retries.
Python (sync driver)¶
from cassandra.cluster import Cluster
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('my_keyspace')
# Prepared statement for idempotent read
select_stmt = session.prepare("SELECT * FROM users WHERE id = ?")
select_stmt.is_idempotent = True # SELECTs are safe to retry
result = session.execute(select_stmt, [user_id])
# Prepared statement for idempotent write (absolute value update)
update_stmt = session.prepare("UPDATE users SET name = ? WHERE id = ?")
bound = update_stmt.bind(['John', user_id])
bound.is_idempotent = True # Setting absolute value is safe to retry
session.execute(bound)
# Prepared statement for non-idempotent write - do NOT mark
counter_stmt = session.prepare("UPDATE counters SET views = views + 1 WHERE page_id = ?")
session.execute(counter_stmt, [page_id]) # Counter increment must not retry
Python (async driver)¶
The async driver includes AsyncRetryPolicy with configurable retry attempts. Read operations (SELECTs) are automatically retried without needing to mark them as idempotent.
from async_cassandra import AsyncCluster
from async_cassandra.retry_policy import AsyncRetryPolicy
# Configure retry policy with max retries
cluster = AsyncCluster(
['127.0.0.1'],
retry_policy=AsyncRetryPolicy(max_retries=5)
)
session = await cluster.connect('my_keyspace')
# Prepared statement for read - automatically retried
select_stmt = await session.prepare("SELECT * FROM users WHERE id = ?")
result = await session.execute(select_stmt, [user_id])
# Prepared statement for idempotent write
insert_stmt = await session.prepare("INSERT INTO users (id, email) VALUES (?, ?) IF NOT EXISTS")
insert_stmt.is_idempotent = True # LWT is safe to retry
await session.execute(insert_stmt, [user_id, '[email protected]'])
# Prepared statement for non-idempotent write - do NOT mark
counter_stmt = await session.prepare("UPDATE counters SET views = views + 1 WHERE page_id = ?")
await session.execute(counter_stmt, [page_id]) # Counter increment must not retry
Java¶
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.cql.*;
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("dc1")
.build();
// Mark simple statement as idempotent
SimpleStatement stmt = SimpleStatement.newInstance(
"SELECT * FROM users WHERE id = ?", userId)
.setIdempotent(true);
session.execute(stmt);
// Prepared statements - set on bound statement
PreparedStatement prepared = session.prepare(
"UPDATE users SET name = ? WHERE id = ?");
BoundStatement bound = prepared.bind("John", userId)
.setIdempotent(true);
session.execute(bound);
Configure retry policy in application.conf:
datastax-java-driver {
advanced.retry-policy {
class = DefaultRetryPolicy
}
# Or use a custom policy
# class = com.example.MyRetryPolicy
}
Built-in Retry Policies¶
| Policy | Behavior |
|---|---|
| DefaultRetryPolicy | Retries reads once if enough replicas responded; never retries writes |
| FallthroughRetryPolicy | Never retries - always rethrows to application |
| DowngradingConsistencyRetryPolicy | Retries at lower consistency level (use with caution) |
Consult your driver's documentation for the complete list of available retry policies and guidance on implementing custom policies for your specific requirements.
Error Handling¶
Common exceptions to handle:
| Exception | Cause | Action |
|---|---|---|
NoHostAvailable |
No nodes reachable | Check connectivity |
ReadTimeout |
Read took too long | Retry or check data model |
WriteTimeout |
Write took too long | Retry or check cluster health |
Unavailable |
Not enough replicas | Check cluster health |
InvalidQuery |
CQL syntax error | Fix query |
Examples¶
Java:
import com.datastax.oss.driver.api.core.servererrors.*;
import com.datastax.oss.driver.api.core.AllNodesFailedException;
try {
session.execute("SELECT * FROM users");
} catch (ReadTimeoutException e) {
System.out.println("Query timed out - consider adjusting timeout or data model");
} catch (UnavailableException e) {
System.out.printf("Not enough replicas: required=%d, alive=%d%n",
e.getRequired(), e.getAlive());
} catch (WriteTimeoutException e) {
System.out.println("Write timed out - check cluster health");
} catch (AllNodesFailedException e) {
System.out.println("Cannot connect to any host: " + e.getAllErrors());
}
Python:
from cassandra import ReadTimeout, Unavailable, NoHostAvailable
try:
session.execute("SELECT * FROM users")
except ReadTimeout:
print("Query timed out - consider adjusting timeout or data model")
except Unavailable as e:
print(f"Not enough replicas: required={e.required_replicas}, alive={e.alive_replicas}")
except NoHostAvailable as e:
print(f"Cannot connect to any host: {e.errors}")
Best Practices¶
Do¶
- ✅ Use prepared statements for repeated queries
- ✅ Set appropriate consistency levels
- ✅ Use token-aware load balancing
- ✅ Handle exceptions gracefully
- ✅ Close sessions and clusters on shutdown
- ✅ Use async for high-throughput workloads
Don't¶
- ❌ Create new sessions for each query
- ❌ Use
ALLOW FILTERINGin production - ❌ Ignore connection pool settings
- ❌ Use
ALLconsistency unnecessarily - ❌ Ignore timeouts and retries
Driver Documentation¶
For detailed driver documentation, refer to the official repositories:
- Java Driver - Apache Cassandra Java Driver
- Spark Cassandra Connector - Apache Spark integration for Cassandra
- Python Driver - Apache Cassandra Python Driver
- Async Python Client - Async Python Cassandra Client
- Node.js Driver - DataStax Node.js Driver
- Go Driver - GoCQL
- C# Driver - DataStax C# Driver
- C++ Driver - Apache Cassandra C++ Driver
- Ruby Driver - DataStax Ruby Driver (maintenance mode)
- PHP Driver - DataStax PHP Driver (maintenance mode)
Next Steps¶
After connecting the application:
- Data Modeling - Design effective schemas
- CQL Reference - Full query language reference
- Performance Tuning - Optimize application performance