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.

Intermediate → Advanced115–155 minutesPaging/API cursor labApache Cassandra 5.0.9 · Java 17 · Apache Java Driver 4.19.3 · RF=3 · LOCAL_QUORUM · UCSLast reviewed: September 2026

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.

01

Explain fetch size, server pages, synchronous transparent fetching, asynchronous explicit page fetching, and paging state.

02

Generate enough deterministic rows to observe multiple protocol page round-trips.

03

Use the Java driver's safe PagingState wrapper and verify statement/value matching.

04

Design an API continuation token that binds query identity, tenant/authorization context, expiry and integrity to the opaque paging state.

05

Reject large unbounded result sets and offset emulation as substitutes for query-first bounded partitions.

Chapter 21 lab baseline

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.

Execution and safety note

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

bash / PowerShell-friendly Docker commands · verify the shared cluster
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;"
CQL · create the driver-focused AtlasMart query table
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');
XML · minimal pinned Maven project for the Apache Java Driver
<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>
Java · seed 30 rows in one bounded month partition
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"));   }  } }}
bash / PowerShell · compile and run on the Cassandra Docker network
# 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

Java · fetch exactly one page, serialize safe continuation state
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.

pseudo-code · application-owned continuation envelope
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

Java · safe state rejects a mismatched bound statement
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);
Boundary: the driver check is not authorization.

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

CQL · server trace a bounded partition read
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

  1. Does fetch size cap the total rows an iterator can consume?
  2. What must match when reusing paging state?
  3. Is safe PagingState cryptographically secure?
  4. Why is OFFSET-style pagination a poor Cassandra default?
  5. 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.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.