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

Connection Pool Sizing, Timeouts, Lifetime, Health Checks, and Avoiding One-Session-per-Request Anti-Patterns

Size and protect the connection pool from measured concurrency and network behavior, keeping sessions short-lived and one Driver long-lived.

Advanced165–210 minutesPool saturation + timeout labNeo4j 2026.07.1 Community · Cypher 25Python driver 6.3.0 · Bolt 6/5 awareLast reviewed: September 2026

Learning outcomes

AtlasMart receives a burst of 200 concurrent API calls and someone proposes “set the pool to 1000.” That number is meaningless without database capacity, per-host semantics, request duration, transaction concurrency and connection budgets. This lesson treats the pool as a queueing boundary with explicit timeouts and failure signals.

01

Explain per-host pool limits and why one long-lived Driver owns multiple reusable connections.

02

Separate connection establishment timeout, pool-acquisition timeout, transaction timeout and retry budget.

03

Use connection lifetime/liveness checks to coexist with firewalls/load balancers without unnecessary churn.

04

Build a bounded concurrent load test that records success, acquisition failures and latency percentiles.

05

Detect the truly harmful anti-pattern: a new Driver per request, not a short-lived Session per request.

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. Correct the anti-pattern wording first

Short-lived sessions are recommended.

The dangerous pattern is one Driver/pool per request or sharing one Session concurrently. A session is lightweight and generally should be scoped to one request/unit of work. The application-lifetime Driver owns the reusable pool.

Configuration Python driver 6.3 default What it limits
max_connection_pool_size 100 per host simultaneous idle + in-use pooled connections for that host
connection_acquisition_timeout 60 s wait to obtain/create a usable pooled connection
connection_timeout 30 s TCP connect establishment only; not the entire query
max_connection_lifetime 3600 s age after which a connection is retired from the pool
liveness_check_timeout None idle age threshold before pre-use liveness testing; disabled by default
max_transaction_retry_time driver-defined documented retry budget; configure deliberately managed-transaction retry horizon, not request deadline

2. Pool size follows concurrency math, not folklore

If an instance receives 40 concurrent database-active requests and each request holds one connection while streaming for 200 ms, a pool of 10 creates queueing by design; a pool of 1000 may only move overload to Neo4j. Start from measured concurrent database occupancy, server connection capacity, number of application instances and cluster hosts.

Python · bounded driver configuration
driver = GraphDatabase.driver(    URI,    auth=AUTH,    max_connection_pool_size=20,    connection_acquisition_timeout=2.0,    connection_timeout=5.0,    max_connection_lifetime=1800.0,    liveness_check_timeout=60.0,    max_transaction_retry_time=10.0,)

The numbers above are a lab starting point, not a production recommendation. In a routing driver the pool limit is per host, so total possible connections can scale with discovered cluster members and application identities.

3. Timeouts answer different failure questions

Timeout/budget Question answered Do not confuse with
connection timeout Can TCP connect to this address promptly? authentication, routing, query runtime
acquisition timeout Can this request obtain a pool connection before its queue budget expires? server query timeout
transaction timeout Can this database unit of work finish before the server terminates it? network connect timeout
managed retry budget How long may the driver retry retryable transaction failures? overall HTTP/SLO deadline
application request deadline How long may the end-to-end API consume? any single driver/server timeout

A good service has an explicit outer SLO/deadline and chooses inner budgets that leave time for useful error handling. A client-side timeout can leave commit outcome ambiguous; do not infer “rolled back” merely because the caller stopped waiting.

4. Health checks: useful but not free

verify_connectivity() is a startup/readiness diagnostic, not a per-request prerequisite. liveness_check_timeout optionally checks connections that have sat idle longer than a threshold; setting it to zero causes a check every acquisition and adds round trips. Usually leave it alone until network equipment is proven to drop idle connections.

Python · readiness probe
def readiness(store: AtlasMartNeo4j) -> tuple[bool, str]:    try:        store.driver.verify_connectivity()        return True, "neo4j reachable"    except Exception as exc:        # map to structured internal diagnostics; do not leak credentials        return False, type(exc).__name__

5. Load-test the pool without hiding failures

Python · bounded concurrent read test
from concurrent.futures import ThreadPoolExecutor, as_completedfrom statistics import medianfrom time import perf_counterQUERY = "CYPHER 25 MATCH (p:Product {productId:$id}) RETURN p.name AS name"def one(i):    t0 = perf_counter()    try:        records, summary, _ = store.driver.execute_query(            QUERY, id="P-1001", database_="neo4j", routing_=RoutingControl.READ)        return {"ok": True, "ms": (perf_counter()-t0)*1000,                "available_ms": summary.result_available_after}    except Exception as exc:        return {"ok": False, "ms": (perf_counter()-t0)*1000,                "error": type(exc).__name__}with ThreadPoolExecutor(max_workers=32) as pool:    results = [f.result() for f in as_completed([pool.submit(one, i) for i in range(200)])]ok = [r["ms"] for r in results if r["ok"]]errors = [r for r in results if not r["ok"]]print("success", len(ok), "errors", len(errors), "median_ms", median(ok) if ok else None)print("error_types", sorted({e["error"] for e in errors}))

Record p50/p95/p99 with an actual percentile library or stable helper, not only median. Repeat at controlled concurrency levels. Pool acquisition failures are valuable evidence: do not “fix” them by removing limits until you know whether the database itself is saturated.

6. Wrong approach: driver creation inside request handler

Python · connection churn
def endpoint(product_id):    with GraphDatabase.driver(URI, auth=AUTH) as driver:  # WRONG hot path        driver.verify_connectivity()                     # WRONG per request        return driver.execute_query(            "MATCH (p:Product {productId:$id}) RETURN p.name",            id=product_id, database_="neo4j")

Each request now creates/destroys a pool and may add connectivity handshakes. The repaired service injects one Driver into repositories and creates sessions/transactions only where the unit of work requires them.

Production judgment

Symptom Candidate mechanism Next evidence
acquisition timeout rises pool saturated or connections held too long active request count, result consumption time, server transaction duration
many new connections/sec driver churn or short lifetime/network resets process lifecycle, pool debug logs, firewall/LB settings
query latency okay but API tail high queue/pool/network/client serialization client/server timestamp breakdown
server overloaded after pool increase client limit was protecting DB server CPU/page cache/query concurrency and SLO
stale idle sockets network device closes idle connections idle age, liveness test experiment, network policy

Check your understanding

  1. Is “one session per request” inherently an anti-pattern?
  2. Is max_connection_pool_size=100 a global cap?
  3. Why can an acquisition timeout be healthy?
  4. Should verify_connectivity() run before every query?
  5. What should determine connection lifetime?
Review the answers

1. No. Sessions are lightweight and short-lived; one Driver per request or concurrent session reuse is the real lifecycle mistake.

2. No. In the Python driver it is per host; routing drivers may have pools for multiple cluster members.

3. It bounds queueing and exposes saturation instead of creating unbounded load.

4. No. Use it for startup/readiness diagnostics, not as per-request tax.

5. Measured network/server behavior such as load-balancer/firewall limits and connection churn, not an arbitrary universal value.

Summary and next step

Pool settings bound resource acquisition; routing and retry policies decide where work goes and how failures are re-attempted. Lesson 4 combines those mechanisms with bookmarks and request correlation.

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.