Chapter 13 · Application Drivers and Bolt: Sessions, Routing, Parameters, Result Streaming, and Connection Pools

Sessions, Transactions, Parameters, Result Cursors/Streams, Fetch Size, and Backpressure

Keep session and result ownership explicit so parameters, lazy streaming and backpressure remain correct under real API concurrency.

Intermediate160–200 minutesStreaming/backpressure labNeo4j 2026.07.1 Community · Cypher 25Python driver 6.3.0 · Bolt 6/5 awareLast reviewed: September 2026

Learning outcomes

An AtlasMart product endpoint returns thousands of orders. If the service eagerly materializes everything, it may turn a modest database query into a large client-memory spike. If it leaks a cursor outside its transaction/session scope, the consumer may fail after the database resources have already been released. This lesson makes row streaming and transaction ownership explicit.

01

Use short-lived sessions and managed transactions without sharing a session concurrently.

02

Parameterize Cypher values and keep result ownership inside the transaction/session scope.

03

Explain lazy Result cursors, fetch size and Bolt backpressure.

04

Distinguish eager execute_query() results from streaming transaction results.

05

Measure time-to-first-record, total consumption time and client memory without claiming fetch size fixes server-side sort/aggregation memory.

Chapter 13 baseline · reviewed 9 September 2026

The mandatory lab continues Neo4j Community 2026.07.1, database neo4j, explicit CYPHER 25 where query-language version matters, container atlasmart-neo4j, loopback Bolt 127.0.0.1:7687, authentication neo4j/atlasmart-course-2026, and no TLS on the loopback-only disposable instance. The current official Python driver is neo4j 6.3.0 (Python 3.10–3.14), whose API supports Bolt 6.0–6.1, Bolt 5.0–5.8 and Bolt 4.4. Neo4j 5.26.30 remains the LTS comparison line.

Evidence boundary

This generation environment does not connect to the AtlasMart Neo4j container, so no handshake, routing-table, pool, TLS, bookmark, latency or retry output is fabricated. Each exercise gives commands and invariants to capture on your machine. True multi-member read/write routing requires an Enterprise cluster or Aura deployment; the mandatory Community path proves the driver/session/pool/stream semantics locally and labels cluster-only observations separately.

1. Driver, session, transaction and result have different lifetimes

Object Cost/concurrency contract Recommended lifetime
Driver expensive; owns pool; thread-safe application/process lifetime
Session lightweight; not thread-safe; at most one active transaction one logical request/unit of work; always close
Managed transaction callback may be retried; must not perform unsafe non-idempotent external side effects one database unit of work
Result lazy cursor backed by connection/transaction state until consumed consume or transform before its owning scope closes

2. Parameterize values; do not build Cypher with user strings

Python · unsafe query text concatenation
# WRONG: input becomes syntax, not dataquery = "MATCH (c:Customer {customerId:'" + customer_id + "'}) RETURN c"session.run(query)
Python · parameterized query
result = session.run(    """    CYPHER 25    MATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)    RETURN o.orderId AS orderId, o.status AS status    ORDER BY o.orderId    """,    customerId=customer_id,)

Parameters keep data separate from syntax, support plan reuse and remove a major injection class. They do not authorize the caller: tenant/user scope must still be expressed in the graph model, query predicate and application authorization policy.

3. Eager convenience vs lazy streaming

API shape Result behavior Use when
driver.execute_query() By default converts the result to an EagerResult and retrieves all records bounded response sets or simple repository methods
session.execute_read/write() + iterate Result records arrive lazily in batches large results, early stop, bounded client memory
session.run() auto-commit lazy result; transaction lifetime is tied to result consumption single-statement special cases; understand commit timing explicitly
Python · stream inside the managed transaction
def stream_order_ids(tx, customer_id):    result = tx.run(        """        CYPHER 25        MATCH (:Customer {customerId:$customerId})-[:PLACED]->(o:Order)        RETURN o.orderId AS orderId        ORDER BY o.orderedAt DESC, o.orderId        """,        customerId=customer_id,    )    for record in result:        yield record["orderId"]# Better for a web response: consume/transform while the session is alive.with store.driver.session(database="neo4j", fetch_size=250) as session:    def collect(tx):        return list(stream_order_ids(tx, "C-1001"))    order_ids = session.execute_read(collect)
Generator boundary

Do not return a generator that still depends on tx/Result after execute_read() has returned. Materialize the bounded API payload inside the callback, or design an application streaming abstraction whose database session remains deliberately open for the consumer lifetime.

4. Fetch size creates client-side backpressure

The Python driver default session fetch_size is 1000. The server sends records in batches; as the client consumes them, the driver requests more. Smaller values can reduce buffered client records but increase Bolt round trips. Larger values reduce round trips but can increase buffered memory. There is no universal number.

Python · compare fetch sizes with the same query
from time import perf_counterimport tracemallocQUERY = """CYPHER 25UNWIND range(1, 20000) AS nRETURN n, n * n AS squared"""for fetch_size in (100, 1000, 5000):    tracemalloc.start()    t0 = perf_counter()    first = None    count = 0    with store.driver.session(database="neo4j", fetch_size=fetch_size) as session:        result = session.run(QUERY)        for record in result:            if first is None:                first = perf_counter() - t0            count += 1    _, peak = tracemalloc.get_traced_memory()    tracemalloc.stop()    print(fetch_size, count, "first_s=", first,          "total_s=", perf_counter()-t0, "peak_bytes=", peak)

Run multiple warm and cold trials and report distributions, not one winning elapsed time. Also remember that ORDER BY, aggregation and other operators may need server memory before records can stream; client fetch size does not rewrite the server plan.

5. Wrong approach: share one session across concurrent requests

Python · unsafe concurrency
SESSION = store.driver.session(database="neo4j")  # WRONG shared mutable sessiondef request_a():    return SESSION.run("RETURN 1").single()def request_b():    return SESSION.run("RETURN 2").single()

Sessions are not thread-safe and only one transaction may be active per session. Share the Driver, not the Session. Each concurrent request/thread/task should obtain its own short-lived session or use Driver.execute_query().

6. AtlasMart result contract

Cypher · deterministic read fixture
CYPHER 25MERGE (c:Customer {customerId:'C-1001'})SET c.name='Mina Rahimi', c.tier='GOLD'MERGE (p:Product {productId:'P-1001'})SET p.name='Trail Camera', p.category='Cameras', p.price=129.90MERGE (o:Order {orderId:'O-5001'})SET o.status='PAID', o.orderedAt=datetime('2026-09-08T16:30:00Z')MERGE (c)-[:PLACED]->(o)MERGE (o)-[r:CONTAINS]->(p)SET r.quantity=1, r.unitPrice=129.90;
Python · repository returns application-owned values
def customer_orders(driver, customer_id: str) -> list[dict]:    def work(tx):        result = tx.run(            """            CYPHER 25            MATCH (:Customer {customerId:$customerId})-[:PLACED]->(o:Order)            RETURN o.orderId AS orderId, o.status AS status,                   toString(o.orderedAt) AS orderedAt            ORDER BY o.orderedAt DESC, o.orderId            """, customerId=customer_id)        return [record.data() for record in result]    with driver.session(database="neo4j", fetch_size=250) as session:        return session.execute_read(work)

The repository exports ordinary Python values, not a live Neo4j cursor. That makes ownership, testing and serialization explicit.

Production judgment

Signal Interpretation Possible action
large client peak memory too many/too-large records materialized stream, paginate, reduce projection, cap API payload
many small Bolt pulls/high RTT cost fetch size too small for network latency measure larger fetch sizes
slow first row despite small fetch server operator must materialize/work before producing rows inspect PROFILE; optimize query/model/index
session concurrency warning/failure session used across threads/tasks create one session per concurrent unit
cursor used after scope closed repository leaked driver-owned resource consume/transform before return

Check your understanding

  1. Is fetch_size=100 always faster than 1000?
  2. Why can a query with ORDER BY still use large server memory with a small fetch size?
  3. Which object is safe to share across threads: Driver or Session?
  4. Why return dictionaries instead of a live Result from a repository?
  5. Does parameterization replace authorization?
Review the answers

1. No. It trades memory/buffering against extra network round trips; measure representative records and latency.

2. The server may need to sort/materialize rows before streaming; fetch size primarily controls record transfer/buffering.

3. Driver. Sessions are lightweight but not thread-safe.

4. It makes resource ownership explicit and avoids a cursor outliving its session/transaction.

5. No. It protects query structure; authorization/tenant predicates are separate requirements.

Summary and next step

Streaming controls how results occupy connections and memory; the connection pool controls how concurrent units obtain those connections. Lesson 3 turns pool sizing and timeouts into measurable capacity decisions.

Authoritative references

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.