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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Quick Start Examples”<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
Section titled “Python”pip install cassandra-driverfrom 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)
Section titled “Python (Async)”For async frameworks like FastAPI and aiohttp. Requires Python 3.12+ and Cassandra 4.0+:
pip install async-cassandraimport asynciofrom 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
Section titled “Node.js”npm install cassandra-driverconst 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 get github.com/gocql/gocqlpackage 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
Section titled “Connection Configuration”Essential Settings
Section titled “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
Section titled “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 Clusterfrom 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
Section titled “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 configuredCqlSession session = CqlSession.builder() .addContactPoint(new InetSocketAddress("127.0.0.1", 9042)) .withLocalDatacenter("dc1") .build();Python:
from cassandra.cluster import Clusterimport 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
Section titled “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
Section titled “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 classpathCqlSession session = CqlSession.builder().build();Configuration Loading Order
Section titled “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
Section titled “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-queryPreparedStatement stmt = session.prepare("SELECT * FROM large_table");session.execute(stmt.bind().setExecutionProfileName("olap"));Programmatic Configuration
Section titled “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
Section titled “Loading from External Files”Load configuration from files outside the classpath:
// From filesystemDriverConfigLoader loader = DriverConfigLoader.fromFile( new File("/etc/myapp/cassandra.conf"));
// From URLDriverConfigLoader loader = DriverConfigLoader.fromUrl( new URL("http://config-server/cassandra.conf"));
CqlSession session = CqlSession.builder() .withConfigLoader(loader) .build();Configuration Reloading
Section titled “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
Section titled “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
Section titled “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 routingCqlSession 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 Clusterfrom 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 DCcluster = Cluster(['node1.dc1.example.com', 'node2.dc1.example.com'])
# When using multi-DC contact points, set local DC explicitly to ensure deterministic routingcluster = 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
Section titled “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
Section titled “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)
Section titled “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) → ClientJava (token-aware is default in driver v4+):
// Token-aware + DC-aware is the default behaviorCqlSession 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 Clusterfrom cassandra.policies import TokenAwarePolicy, DCAwareRoundRobinPolicy
# Wrap DC-aware policy with token-aware for best performancecluster = Cluster( ['127.0.0.1'], load_balancing_policy=TokenAwarePolicy( DCAwareRoundRobinPolicy(local_dc='dc1') ))Python (async driver):
from async_cassandra import AsyncClusterfrom 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
Section titled “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 Clusterfrom 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
Section titled “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
Section titled “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
Section titled “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?
Section titled “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
Section titled “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 timesfrom 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 valuesimport uuidsession.execute(insert_stmt, [uuid.uuid4(), 'john_doe', 'john@example.com'])session.execute(insert_stmt, [uuid.uuid4(), 'jane_doe', 'jane@example.com'])// Java - Prepare once, execute many timesPreparedStatement insertStmt = session.prepare( "INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)");
// Execute many timesBoundStatement bound = insertStmt.bind(UUID.randomUUID(), "john_doe", "john@example.com");session.execute(bound);# Python (async) - Prepare once, execute many timesfrom async_cassandra import AsyncCluster
cluster = AsyncCluster(['127.0.0.1'])session = await cluster.connect('my_keyspace')
# Prepare onceinsert_stmt = await session.prepare( "INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)")
# Execute many timesawait session.execute(insert_stmt, [user_id, 'john_doe', 'john@example.com'])Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Preparing the Same Statement Repeatedly
Section titled “❌ Preparing the Same Statement Repeatedly”Java:
// WRONG - Prepares on every call, wasting resourcespublic 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, reuseprivate 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 resourcesdef 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, reuseinsert_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
Section titled “❌ 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 preparesession.prepare("SELECT * FROM users WHERE username = '" + username + "'");
// RIGHT - Use bind parametersPreparedStatement 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 preparesession.prepare(f"SELECT * FROM users WHERE username = '{username}'")
# RIGHT - Use bind parametersstmt = 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
Section titled “❌ Dynamic Table or Column Names”Java:
// WRONG - Cannot use bind parameters for table/column namesString 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 startupprivate 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 namestable = "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 startupuser_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
Section titled “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 statsnodetool info | grep -i preparedCassandra configuration (cassandra.yaml):
# Maximum number of prepared statements to cache (default: 10000)prepared_statements_cache_size_mb: 100Prepared 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
Section titled “Consistency Levels”Set consistency per query:
# Pythonfrom cassandra import ConsistencyLevelfrom cassandra.query import SimpleStatement
stmt = SimpleStatement( "SELECT * FROM users WHERE user_id = %s", consistency_level=ConsistencyLevel.QUORUM)session.execute(stmt, [user_id])// Javasession.execute( SimpleStatement.newInstance("SELECT * FROM users WHERE user_id = ?", userId) .setConsistencyLevel(DefaultConsistencyLevel.QUORUM));Consistency Level Reference
Section titled “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
Section titled “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
Section titled “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)
Section titled “Python (asyncio)”Use the Async Python Cassandra Client for true async/await support with frameworks like FastAPI and aiohttp:
import asyncioimport uuidfrom async_cassandra import AsyncCluster
# Long-lived cluster and session (create once, reuse for all requests)cluster = Nonesession = 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 upasync 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 flowingJava (CompletionStage)
Section titled “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)
Section titled “Node.js (Promise-based)”// Execute multiple queries concurrentlyconst 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
Section titled “Connection Pooling”Drivers maintain connection pools automatically. Key settings:
Python (sync driver)
Section titled “Python (sync driver)”from cassandra.cluster import Clusterfrom 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)
Section titled “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))datastax-java-driver { advanced.connection { pool { local.size = 4 remote.size = 2 } }}Retry Policies
Section titled “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
Section titled “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)
Section titled “Python (sync driver)”from cassandra.cluster import Cluster
cluster = Cluster(['127.0.0.1'])session = cluster.connect('my_keyspace')
# Prepared statement for idempotent readselect_stmt = session.prepare("SELECT * FROM users WHERE id = ?")select_stmt.is_idempotent = True # SELECTs are safe to retryresult = 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 retrysession.execute(bound)
# Prepared statement for non-idempotent write - do NOT markcounter_stmt = session.prepare("UPDATE counters SET views = views + 1 WHERE page_id = ?")session.execute(counter_stmt, [page_id]) # Counter increment must not retryPython (async driver)
Section titled “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 AsyncClusterfrom async_cassandra.retry_policy import AsyncRetryPolicy
# Configure retry policy with max retriescluster = AsyncCluster( ['127.0.0.1'], retry_policy=AsyncRetryPolicy(max_retries=5))session = await cluster.connect('my_keyspace')
# Prepared statement for read - automatically retriedselect_stmt = await session.prepare("SELECT * FROM users WHERE id = ?")result = await session.execute(select_stmt, [user_id])
# Prepared statement for idempotent writeinsert_stmt = await session.prepare("INSERT INTO users (id, email) VALUES (?, ?) IF NOT EXISTS")insert_stmt.is_idempotent = True # LWT is safe to retryawait session.execute(insert_stmt, [user_id, 'john@example.com'])
# Prepared statement for non-idempotent write - do NOT markcounter_stmt = await session.prepare("UPDATE counters SET views = views + 1 WHERE page_id = ?")await session.execute(counter_stmt, [page_id]) # Counter increment must not retryimport 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 idempotentSimpleStatement stmt = SimpleStatement.newInstance( "SELECT * FROM users WHERE id = ?", userId) .setIdempotent(true);session.execute(stmt);
// Prepared statements - set on bound statementPreparedStatement 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
Section titled “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
Section titled “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
Section titled “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
Section titled “Best Practices”- ✅ 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
- ❌ Create new sessions for each query
- ❌ Use
ALLOW FILTERINGin production - ❌ Ignore connection pool settings
- ❌ Use
ALLconsistency unnecessarily - ❌ Ignore timeouts and retries
Driver Documentation
Section titled “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
Section titled “Next Steps”After connecting the application:
- Data Modeling - Design effective schemas
- CQL Reference - Full query language reference
- Performance Tuning - Optimize application performance