Chapter 21 · Drivers, Prepared Statements, Token Awareness, Paging, Retries, and Load Balancing
Paging State, Fetch Size, Large Result Sets, and API Pagination Safety
Treat fetch size and paging state as protocol continuation controls, then wrap paging state safely for authorized stateless APIs without offset-style scans.
Learning outcomes
AtlasMart exposes “next page” links for a customer's monthly
orders. A developer proposes SQL-style
OFFSET 5000 and another wants to accept a raw
Cassandra paging token from any request. Cassandra paging is
neither of those: it is forward-only protocol continuation state
tied to one exact query and values.
Explain fetch size, server pages, synchronous transparent fetching, asynchronous explicit page fetching, and paging state.
Generate enough deterministic rows to observe multiple protocol page round-trips.
Use the Java driver's safe PagingState wrapper and verify statement/value matching.
Design an API continuation token that binds query identity, tenant/authorization context, expiry and integrity to the opaque paging state.
Reject large unbounded result sets and offset emulation as substitutes for query-first bounded partitions.
The mandatory labs continue the established free/local
AtlasMart cluster: Docker Official Image
cassandra:5.0.9, Java 17 in that image, cluster
atlasmart-course, Docker network
atlasmart-cassandra, nodes
atlasmart-cass-1..3, datacenter dc1,
racks rack1..rack3, 16 virtual nodes per node,
and named disposable data volumes. Keyspace
atlasmart_driver uses
NetworkTopologyStrategy with replication factor
(RF) 3; ordinary reads/writes use LOCAL_QUORUM.
New tables explicitly use UnifiedCompactionStrategy (UCS), no
table default time-to-live (TTL), and Cassandra's normal
gc_grace_seconds. Authentication,
client/internode Transport Layer Security (TLS), and remote
Java Management Extensions (JMX) are disabled only on this
isolated single-host learning network. Application examples
use Apache Cassandra Java Driver
4.19.3
(org.apache.cassandra:java-driver-core:4.19.3)
with Java 17 and one long-lived CqlSession.
Driver documentation URLs still use the 4.19.0 documentation
set, while the ASF release is 4.19.3. Exact coordinator
choices, pool counts, traces, paging states, retry/speculation
counts, and latency percentiles are learner-captured runtime
evidence.
Run commands only against the disposable Apache Cassandra course lab or another explicitly approved non-production environment. Confirm node, keyspace, table, container, volume, path, and datacenter targets before destructive, failure-injection, cleanup, repair, restore, security, or topology operations. Capture current state and expected rollback/recovery evidence first; output and timings can differ by host, operating system, Java runtime, Docker/runtime, driver, and Cassandra configuration.
Terms and request-path mental model
The native protocol is Cassandra's binary client/server protocol over TCP, normally on port 9042. A driver implements that protocol for an application language. A contact point is an initial address used to bootstrap discovery; it is not a permanent leader or a complete static node list. A session is the driver's long-lived view of a cluster and owns topology metadata, control-plane state, connection pools, policies, prepared-statement caches, and request execution. A connection pool is the driver's set of TCP connections to a node; unlike a JDBC-style blocking pool, each Cassandra connection multiplexes many in-flight requests using native-protocol stream identifiers.
A request is sent to a coordinator, the Cassandra node that handles that request. The driver chooses that coordinator using a load-balancing policy. Token-aware routing prefers replicas for the target partition when the statement supplies a keyspace and routing key/token. The partition key hashes to a token; replicas own token ranges according to the keyspace replication strategy. Prepared statements let the server parse CQL once and return metadata, including bind-variable types and partition-key variable positions, which enables the driver to compute routing keys for bound statements. Paging splits a large result into multiple protocol responses; the paging state is an opaque continuation token tied to the exact statement and values. Idempotent means repeating a request has the same final database effect as executing it once. Retry and speculative-execution policies use that property to avoid duplicating unsafe mutations.
1. Fetch size is a network-page control, not an application SLA
A page size tells the driver/server how many rows to request per
network page. The synchronous Java API can transparently fetch
later pages while the application iterates, which makes it easy
to accidentally read an enormous partition even though no single
response was huge. The asynchronous API exposes
hasMorePages()/fetchNextPage()
explicitly.
Paging state resumes at the next page for the exact same query string and parameters. It is opaque and forward-only. It is not a row offset, stable cursor across arbitrary query changes, tenant authorization token, or encrypted secret.
2. Create a deterministic multi-page partition
docker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 java -versiondocker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 cqlsh -e "SELECT cluster_name,data_center,rack,release_version,native_protocol_version FROM system.local;"docker exec atlasmart-cass-1 cqlsh -e "SELECT peer,peer_port,data_center,rack,release_version FROM system.peers_v2;"
CREATE KEYSPACE IF NOT EXISTS atlasmart_driverWITH replication = {'class':'NetworkTopologyStrategy','dc1':3};CREATE TABLE IF NOT EXISTS atlasmart_driver.orders_by_customer_month ( tenant_id text, customer_id text, order_month date, order_time timestamp, order_id uuid, status text, total decimal, note text, PRIMARY KEY ((tenant_id,customer_id,order_month),order_time,order_id)) WITH CLUSTERING ORDER BY (order_time DESC,order_id ASC) AND compaction = {'class':'UnifiedCompactionStrategy'};CONSISTENCY LOCAL_QUORUM;INSERT INTO atlasmart_driver.orders_by_customer_month(tenant_id,customer_id,order_month,order_time,order_id,status,total,note)VALUES ('tenant-a','cust-42','2026-09-01','2026-09-08T08:00:00Z',21000000-0000-0000-0000-000000000001,'PAID',129.90,'baseline');INSERT INTO atlasmart_driver.orders_by_customer_month(tenant_id,customer_id,order_month,order_time,order_id,status,total,note)VALUES ('tenant-a','cust-42','2026-09-01','2026-09-08T08:10:00Z',21000000-0000-0000-0000-000000000002,'PACKING',59.00,'fragile');INSERT INTO atlasmart_driver.orders_by_customer_month(tenant_id,customer_id,order_month,order_time,order_id,status,total,note)VALUES ('tenant-a','cust-99','2026-09-01','2026-09-08T08:20:00Z',21000000-0000-0000-0000-000000000003,'PAID',210.00,'priority');
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>academy.atlasmart</groupId><artifactId>cassandra-driver-lab</artifactId><version>1.0.0</version> <properties><maven.compiler.release>17</maven.compiler.release></properties> <dependencies> <dependency> <groupId>org.apache.cassandra</groupId> <artifactId>java-driver-core</artifactId> <version>4.19.3</version> </dependency> <dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>2.0.17</version></dependency> </dependencies> <build><plugins><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version></plugin></plugins></build></project>
package academy.atlasmart;import com.datastax.oss.driver.api.core.*;import com.datastax.oss.driver.api.core.cql.*;import java.net.*; import java.time.*; import java.util.*;public class DriverLab { public static void main(String[] a){ try(CqlSession s=CqlSession.builder().addContactPoint(new InetSocketAddress("atlasmart-cass-1",9042)).withLocalDatacenter("dc1").build()){ PreparedStatement ins=s.prepare("INSERT INTO atlasmart_driver.orders_by_customer_month (tenant_id,customer_id,order_month,order_time,order_id,status,total,note) VALUES (?,?,?,?,?,?,?,?)"); LocalDate month=LocalDate.parse("2026-09-01"); for(int i=0;i<30;i++){ s.execute(ins.bind("tenant-a","cust-page",month,Instant.parse(String.format("2026-09-%02dT12:00:00Z",1+(i%28))),new UUID(0,1000+i),"PAID",java.math.BigDecimal.valueOf(i+1),"page-fixture")); } } }}
# From the Maven project directory. Docker Desktop/WSL or Linux/macOS shell:docker run --rm --network atlasmart-cassandra -v "$PWD:/work" -w /work maven:3.9.16-eclipse-temurin-17 \ mvn -q -DskipTests compile exec:java -Dexec.mainClass=academy.atlasmart.DriverLab# PowerShell uses the same container and network; ${PWD} resolves to the current directory:# docker run --rm --network atlasmart-cassandra -v "${PWD}:/work" -w /work maven:3.9.16-eclipse-temurin-17 `# mvn -q -DskipTests compile exec:java -Dexec.mainClass=academy.atlasmart.DriverLab
3. Capture a safe paging state and resume
PreparedStatement ps=s.prepare("SELECT order_time,order_id,status,total FROM atlasmart_driver.orders_by_customer_month WHERE tenant_id=? AND customer_id=? AND order_month=?");BoundStatement first=ps.bind("tenant-a","cust-page",LocalDate.parse("2026-09-01")).setPageSize(7);ResultSet rs=s.execute(first);int available=rs.getAvailableWithoutFetching();for(int i=0;i<available;i++) System.out.println(rs.one());PagingState state=rs.getExecutionInfo().getSafePagingState();String token=(state==null?null:state.toString());System.out.println("nextToken="+token);// Later, rebuild the EXACT same bound statement and validate the safe wrapper.if(token!=null){ PagingState parsed=PagingState.fromString(token); BoundStatement next=ps.bind("tenant-a","cust-page",LocalDate.parse("2026-09-01")) .setPageSize(7).setPagingState(parsed); ResultSet page2=s.execute(next); int n=page2.getAvailableWithoutFetching(); for(int i=0;i<n;i++) System.out.println(page2.one());}
The token string will vary. The safe wrapper performs statement/value matching, but it is not cryptographically secure; documentation explicitly warns that its serialized form is opaque rather than encrypted/authenticated. A public API should therefore wrap it in an application token that binds tenant/user/query version/parameters/expiry and adds integrity protection.
payload = { tenant_id: authenticatedTenant, query_id: "orders-by-customer-month-v1", customer_id: authorizedCustomer, order_month: "2026-09-01", cassandra_paging_state: driverSafeStateString, expires_at: 2026-09-08T13:00:00Z}api_token = base64url(payload) + HMAC(server_secret, payload)# On resume: verify HMAC/expiry/authorization FIRST, then reconstruct the exact statement and apply paging state.
4. Deliberately wrong: reuse a token with another customer's query
PagingState state = PagingState.fromString(token);BoundStatement wrong = ps.bind("tenant-a","cust-99",LocalDate.parse("2026-09-01")).setPageSize(7);// setPagingState(state) should fail fast because bound values do not match the original statement.wrong = wrong.setPagingState(state);
Even a valid paging state must not let a caller switch tenant/customer or bypass row-level business authorization. Validate authorization from trusted application context and bind it into the signed continuation envelope. Never treat a raw Cassandra token as a bearer credential.
5. Observe coordinator/page round-trips and latency
TRACING ON;SELECT order_time,order_id,status,totalFROM atlasmart_driver.orders_by_customer_monthWHERE tenant_id='tenant-a' AND customer_id='cust-page' AND order_month='2026-09-01';TRACING OFF;
In the Java lab, record one ExecutionInfo per
fetched page and the client time for each page. Report
p50/p95/p99 only after enough repetitions and warmup; do not
compare a 7-row page to a 500-row page without recording
payload/result size. If a result is routinely huge, fix the
query/partition/API contract rather than relying on paging to
make unbounded work “safe.”
Check your understanding
- Does fetch size cap the total rows an iterator can consume?
- What must match when reusing paging state?
- Is safe PagingState cryptographically secure?
- Why is OFFSET-style pagination a poor Cassandra default?
- What should a public continuation token bind beyond Cassandra state?
Review the answers
1. No. It caps one page/network response; transparent iteration can fetch more pages.
2. The exact statement/query and bound values; the state is a continuation of that execution shape.
3. No. It adds statement matching, not encryption/strong authenticity; wrap it for public APIs.
4. It implies skipping work client-side and does not match Cassandra forward paging/query-first access patterns.
5. Authorization/tenant context, query identity/version, relevant parameters, expiry and integrity protection.
Production judgment
Client correctness depends on more than successful TCP connectivity. Record Cassandra patch/native protocol negotiation, driver artifact/version, Java runtime, local datacenter, discovered topology, node distance, pool/in-flight metrics, prepared-statement/cache behavior, routing-key availability, page/fetch size, request/consistency/serial-consistency timeouts, retry policy, idempotence classification, speculative-execution policy, per-query execution profile, and p50/p95/p99 client latency. Correlate driver coordinator/attempt data with Cassandra tracing, server timeouts/failures, replica availability, RF/CL, partition size/cardinality, SSTable/compaction/tombstone state, disk/network/JVM pressure, repair state, and SAI/vector costs where those query paths are used.
Do not make a session per HTTP request, disable local-DC awareness casually, expose raw paging state as an authorization token, or enable broad retries/speculation to hide overload. Treat driver configuration as application production code: version it, test node/DC failures, test ambiguous timeouts with idempotent and non-idempotent mutations, measure extra traffic, and define rollback. Managed services may provide different endpoints, TLS/auth requirements, topology visibility, or restricted metrics while still using a Cassandra-compatible protocol; verify the service contract rather than assuming identical behavior. Lesson 4 returns to routing and shows how the default policy converts local-datacenter metadata, node distance/state, token information and failure observations into a coordinator query plan.
Summary and next bridge
Paging controls result transport, not data-model cost. Paging state is exact-query continuation state and must be treated as opaque, scoped and integrity-protected at API boundaries. Next, inspect local-DC load balancing and request routing during node failure.
Authoritative references
Driver behavior is language- and version-specific. Re-check the exact maintained driver and Cassandra patch before freezing production defaults or error-handling behavior.
- Apache Cassandra downloads / current 5.0 patch
- Docker Official Cassandra 5.0 image source
- Apache Cassandra Java Driver overview
- Java Driver pooling
- Java Driver prepared statements
- Java Driver load balancing / token awareness
- Java Driver paging
- Java Driver retries
- Java Driver idempotence
- Java Driver speculative execution
- Maven Central Java Driver 4.19.3