Drivers, prepared statements and paging

Apache Cassandra · Practical · 11 min

Application performance with Cassandra depends as much on driver usage as on schema.

Applications use a driver (Java, Python, Go, Node.js, C#...) speaking the native protocol on port 9042. Core practices are the same in every language:

  • create one session per application and reuse it; it manages connection pools to all nodes,
  • use prepared statements for anything executed repeatedly: the statement is parsed once, values are bound as parameters (no CQL injection), and the driver learns which columns form the partition key,
  • keep the default token-aware load balancing policy (wrapped in a data-center-aware one): the driver computes the token of the bound partition key and sends the request directly to a replica, saving a network hop,
  • set the consistency level per statement or as a profile default (LOCAL_QUORUM is typical),
  • run independent writes asynchronously and in parallel, with a cap on in-flight requests, instead of batching them.

Large results are paged automatically: the driver fetches fetch_size rows (5000 by default) at a time as you iterate. For stateless APIs, return the opaque paging state to the client and resume with it.

Mark statements idempotent when they are (most upserts are; counter updates and list appends aren't) so the driver may retry or speculatively execute them safely after timeouts.

Don't bind null for missing values; leave them unset to avoid tombstones.

Example

from datetime import date, datetime, timezone
from cassandra.cluster import Cluster, ExecutionProfile, EXEC_PROFILE_DEFAULT
from cassandra.policies import DCAwareRoundRobinPolicy, TokenAwarePolicy
from cassandra import ConsistencyLevel

profile = ExecutionProfile(
    load_balancing_policy=TokenAwarePolicy(DCAwareRoundRobinPolicy(local_dc="datacenter1")),
    consistency_level=ConsistencyLevel.LOCAL_QUORUM)
cluster = Cluster(["10.0.0.1", "10.0.0.2"], execution_profiles={EXEC_PROFILE_DEFAULT: profile})
session = cluster.connect("shop")                 # one session per app

insert = session.prepare("INSERT INTO readings (sensor_id, day, ts, value) VALUES (?, ?, ?, ?)")
insert.is_idempotent = True
day = date(2026, 10, 17)
rows = [(day, datetime(2026, 10, 17, 9, m, tzinfo=timezone.utc), 20 + m / 10) for m in range(60)]
futures = [session.execute_async(insert, ("s-17", day, ts, v)) for day, ts, v in rows]
for f in futures:
    f.result()                                    # raise on failure

select = session.prepare("SELECT ts, value FROM readings WHERE sensor_id = ? AND day = ?")
for row in session.execute(select, ("s-17", day)):  # pages transparently
    print(row.ts, row.value)

Remember

  • One long-lived session per application
  • Prepared statements: parsed once, safe binding, enable token-aware routing
  • Run independent writes async in parallel, not in batches
  • Results page automatically; mark idempotent statements

Watch out for

  • Building CQL strings with f-strings for each request: no routing information, re-parsing, and injection risk.

Check yourself

  1. Why do prepared statements make token-aware routing possible?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: cassandra.apache.org/doc/latest/cassandra/getting-started/drivers.html

Builds on

Unlocks

Nothing builds on this yet.