Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bolt & Neo4j Browser

The server speaks the native Bolt protocol, so the official Neo4j drivers and the Neo4j Browser connect to patinaDB directly. This has been validated end-to-end against Neo4j driver 6.2.0 (over both raw TCP and WebSocket) and the Neo4j Browser (node creation plus path/relationship graph visualisation).

Connecting

The Bolt listener (default 127.0.0.1:7687) auto-detects the transport: a raw Bolt TCP handshake or a WebSocket upgrade (the Browser’s JavaScript driver speaks Bolt inside WebSocket binary frames). Both work on the same port.

Neo4j Browser: open browser.neo4j.io, connect to bolt://localhost:7687, authenticate with --auth-user / --auth-password.

Official driver (Python):

from neo4j import GraphDatabase
drv = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "secret"))
with drv.session() as s:
    s.run("CREATE (a:Person {name:$n})", n="Ada")
    for rec in s.run("MATCH (n:Person) RETURN n"):
        print(rec["n"])
drv.close()

Nodes, relationships, and paths come back as proper Bolt graph types (Node / Relationship / Path), so drivers hydrate them as graph objects and the Browser visualises them.

Protocol version negotiation: the server negotiates whatever Bolt version a client offers during the handshake, including modern 5.x. A client that only speaks an older version still works, with reduced fidelity where that version’s wire format is narrower — for example, a client negotiating Bolt 4.4 (some non-official/community drivers, e.g. Rust’s neo4rs 0.9, only offer 4.4) does not receive the newer element_id field on nodes/relationships; use id(n) in Cypher instead if you need a stable per-node identifier over such a client.

Spatial values over Bolt

A patinaDB Point is sent as a native Neo4j Point struct, so official drivers decode it as a first-class point (neo4j.spatial.Point / a driver’s Point type):

  • a 2-D point → PackStream struct tag 0x58 with fields {srid: Integer, x: Float, y: Float};
  • a 3-D point → tag 0x59 with an added z: Float.

The srid is the CRS discriminant patinaDB already stores — cartesian 7203, cartesian-3d 9157, wgs-84 4326, wgs-84-3d 4979 (the same SRIDs Neo4j uses). For wgs-84 the point’s x is the longitude and y the latitude.

A point can also be passed as a parameter: send $center as a 0x58/0x59 struct and it decodes back into a point you can use in Cypher, e.g. RETURN distance($center, n.loc).

Polygons have no native Bolt type. Bolt only defines Point2D/Point3D, so a patinaDB Polygon is returned as a Map{type: "Polygon", srid: Integer, rings: [[[x, y], …], …]} (rings[0] is the exterior ring, rings[1..] the holes). A driver therefore sees a plain map, not a spatial object; decode it yourself if you need the geometry.

Authentication

Auth is checked at LOGON (scheme: "basic"). An unauthenticated connection cannot run queries. See Authentication & TLS.

Streaming reads

Read results stream lazily: the query runs on a worker thread that pushes hydrated records over a bounded channel, with PULL n batching and backpressure. Memory stays bounded regardless of result size, so you can stream large result sets without materialising them all server-side.

Transactions

Both autocommit and explicit transactions are real.

  • Autocommit (a bare RUN): each statement commits on its own as one engram — atomic, even for a multi-clause or bulk statement.
  • Explicit (BEGIN … COMMIT / ROLLBACK): statements between BEGIN and COMMIT are buffered, not committed one by one. BEGIN pins a consistent snapshot of the database; every RUN sees that snapshot plus the transaction’s own uncommitted writes (repeatable reads + read-your-own-writes); COMMIT applies the whole buffer as one atomic engram (a single Raft entry on a cluster) after a conflict check; ROLLBACK discards it — nothing is written.

Explicit transactions run at snapshot isolation (SI). A transaction is bound to one database — a mid-transaction USE <other> is rejected — and schema/admin statements (e.g. CREATE INDEX, CREATE DATABASE) run eagerly, not buffered. The official Neo4j drivers’ managed transaction functions work as expected, including automatic retry on the conflict error below.

Isolation: explicit transactions are snapshot-isolated, not serializable

  • BEGIN pins the database HEAD (the serialization point). Every read in the transaction observes the graph as of that moment — a commit by another connection made after your BEGIN is invisible to your transaction (repeatable reads, no phantoms). Your own buffered writes are visible to your own reads (read-your-own-writes).
  • COMMIT is first-committer-wins. Before proposing the buffer, the server checks whether any write committed since your pinned snapshot touched an entity/property your transaction also wrote. If so, the COMMIT is rejected with a transient error (Neo.TransientError.Transaction.LockClientStopped), which the Neo4j drivers’ managed-transaction functions retry automatically. This prevents lost updates (e.g. two clients that both “read a counter, increment, write it back” — one commits, the other retries against the new value instead of silently clobbering it).
  • Autocommit (a bare single-statement RUN) holds the per-database write lock for the statement, so autocommit writes to one database are serialized.

What SI does not give you: write skew

Snapshot isolation detects write-write conflicts only. A read-write conflict with disjoint write-sets (write skew) is still possible: two transactions can each read a value the other overwrites, as long as they write different things, and both commit. This is the standard, well-understood SI limitation — it is not serializable and not linearizable. If you need a constraint that spans rows one transaction reads and another writes (e.g. “at most one on-call engineer”), enforce it in a single autocommit statement or with an application-level guard. See Limitations.

Conflict granularity

Property writes conflict at (entity, property) granularity (two transactions updating different properties of the same node do not falsely conflict); create/delete and label changes conflict at whole-entity granularity. Freshly-created nodes carry new UUIDs, so concurrent inserts never conflict.

Causal consistency (bookmarks)

A standard Neo4j driver session gets read-your-writes automatically: the driver threads an opaque bookmark from each statement’s result into the next session.run call, and the server blocks that next read until it has caught up to the bookmark. This now actually works end-to-end against patinaDB — no client code change needed, since it’s the driver’s default session behaviour.

Concretely:

  • Every successful autocommit write (a bare RUN) and every successful COMMIT returns a bookmark encoding the Raft log index that write was committed at.
  • A subsequent BEGIN or autocommit RUN carrying that bookmark blocks until this connection’s node has applied that index, before doing anything else — for BEGIN, before it pins its snapshot-isolation snapshot, so the pinned snapshot itself already reflects the bookmarked write.
  • Once caught up (the common case, especially against the leader — no wait at all), the statement runs as normal.

This matters most when a routing (neo4j://) driver load-balances a session’s reads across followers (see cluster-aware routing below): without a bookmark, a write-then-read session could land its read on a follower that hasn’t replicated the write yet and get a stale result with no error. With bookmarks, that same session either waits briefly for the follower to catch up, or times out loudly — it never silently returns stale data.

The wait is bounded: --bookmark-wait-timeout-secs (default 10 seconds; see Configuration Reference) caps how long a node will wait for its own applied state to catch up to an incoming bookmark. If that bookmark can’t be reached in time, the client gets a driver-retryable Neo.TransientError.Transaction.BookmarkTimeout — the driver retries, and its routing may pick a more caught-up member — never a silent stale read. Set --bookmark-wait-timeout-secs 0 to disable the wait entirely (bookmarks are then parsed but ignored, reverting to plain possibly-stale-follower reads).

Read-consistency by surface

Different ways of talking to patinaDB give different freshness guarantees for a read that follows a write:

SurfaceGuarantee
Bolt driver session (bookmarks)Read-your-writes — automatic, bounded-wait (above). No client change needed.
REST POST /cypher, "consistency":"local" (default)Eventually consistent on a follower; fast, no wait.
REST POST /cypher, "consistency":"linearizable"Read-your-latest-write against live HEAD — leader-only, costs a round-trip. See REST API.
A plain follower read with no bookmark / no linearizable flagEventually consistent — bounded staleness. The neo4j:// routing table already excludes chronically-lagging followers (see High Availability).

See High Availability → Read consistency for the full discussion, including the lag-immune AS OF TAG pattern for reads that must be reproducible across followers with no leader round-trip at all.

Connection resource limits (unauthenticated slowloris hardening)

Three coordinated guards close an unauthenticated connection-exhaustion attack (issue #440): an attacker who opens Bolt sockets and either sends nothing, stalls partway through the version-negotiation handshake, or completes HELLO/LOGON and then idles, used to hold a --max-bolt-connections permit indefinitely — a few thousand such connections was a complete driver-protocol lockout, not merely file-descriptor pressure.

  • --bolt-handshake-timeout-secs (PATINADB_BOLT_HANDSHAKE_TIMEOUT_SECS, default 10): bounds every pre-authentication step an accepted socket can stall on — the TLS ClientHello (--tls-cert), the raw-Bolt-vs-WebSocket dispatch first-byte peek, the WebSocket HTTP upgrade (the Neo4j Browser transport), and the 20-byte Bolt version-negotiation handshake. So on all three transports (plain TCP, TLS, WebSocket) a socket that sends nothing at all, stalls partway through the TLS/WebSocket handshake, or sends fewer than 20 Bolt handshake bytes and then stalls, is closed cleanly once this deadline elapses instead of pinning the connection task forever. Set 0 to disable.
  • --bolt-idle-timeout-secs (PATINADB_BOLT_IDLE_TIMEOUT_SECS, default 300): bounds how long a connection may sit with no open explicit transaction and no started-but-unconsumed result stream — those two states have their own dedicated reapers, --idle-tx-timeout-secs and --stream-pull-timeout-secs below — while waiting for the next client message. Covers a connection that finishes the handshake and/or HELLO/LOGON and then goes silent. The clock resets on every client message, so a well-behaved driver never trips it. Set 0 to disable.
  • The --max-bolt-connections permit is acquired only after a successful LOGON (or, with --insecure-disable-auth, on a connection’s first message — auth-disabled connections have no separate LOGON event to key off of), never at TCP accept time. An unauthenticated connection — silent, stalled mid-handshake, or idling after HELLO — therefore never consumes the connection budget meant for real, authenticated clients; it is bounded purely in time by the two deadlines above. A connection that is over the cap at the moment it would authenticate gets a Neo.TransientError.General.DatabaseUnavailable failure (driver-retryable) in place of its normal authentication success, and the socket is closed.

Transaction resource limits

Two operational guards bound an explicit transaction so a runaway or abandoned one can’t exhaust server resources (both default ON, opt out with 0):

  • --max-tx-ops (PATINADB_MAX_TX_OPS, default 5000000): the maximum number of resolved write operations an explicit transaction may buffer across all its statements before COMMIT. The buffer commits as one Raft entry, so this bounds connection RAM and the entry size. Exceeding it aborts and rolls back the transaction with a driver-retryable transient error — split a very large load into smaller transactions (or use CALL { … } IN TRANSACTIONS, which commits in bounded chunks). Each individual statement is separately bounded by PATINADB_MAX_CAPTURE_OPS.
  • --idle-tx-timeout-secs (PATINADB_IDLE_TX_TIMEOUT_SECS, default 300): the “idle in transaction” guard. An explicit transaction that has been open with no client activity for this long is rolled back and its connection is closed by a background reaper — so an abandoned BEGIN (a client that opened a transaction and then went away without COMMIT/ROLLBACK) can’t hold resources forever. The clock resets on every statement, so a slow-but-active transaction is never reaped. Set 0 to disable the reaper.

Database selection

The target database is taken per-RUN / per-BEGIN (from the Bolt db field or a USE <db> prefix) and is never sticky across autocommit statements — connections are pooled, so each statement resolves its own database. See Multi-Database.

System / introspection shim

On connect, the Neo4j Browser fires admin/introspection statements that are not ordinary Cypher (CALL dbms.components(), SHOW DATABASES, CALL db.labels(), routing-table lookups, …). The server recognises these and returns canned/registry-derived results so the Browser connects cleanly, shows a server version, and populates its database dropdown. Real procedures (notably CALL db.index.fulltext.*) are not swallowed by the shim — they reach the engine and return real data.

Cluster-aware routing

neo4j:// (routing) drivers ask the server for a routing table and then connect to the addresses it returns. In a cluster, patinaDB answers with the real topology — writes to the leader, reads spread across the followers — so a routing driver offloads reads to replicas automatically. See High Availability → Cluster-aware Bolt routing for the full WRITE/READ/ROUTE breakdown and the lag-immune clean-tag read pattern. A single-node server returns itself for all three roles.

Behind a reverse proxy

Behind a TLS-terminating proxy or load balancer, set --advertised-addr to the public host:port so the routing table sends drivers to a reachable endpoint (each node advertises its own). For a direct bolt:// connection this isn’t needed — the default advertises the listen address. See Authentication & TLS.