bolty can talk to a Neo4j Enterprise causal cluster using server-side
routing (SSR): you point a connection at one cluster member with a neo4j://
scheme, and the server forwards each statement to the member that can serve it —
a writable member for writes, a read member for reads — based on the query's
access mode. This page covers what SSR does, how to enable it, and the caveats
that matter before you rely on it in production.
Scope
SSR gives you correct routing (writes reach a leader, reads reach a read member) from a single configured entry point. It is not full driver-side cluster routing: bolty does not discover the cluster topology, load-balance across read replicas, or fail over automatically if the configured member goes down. See Caveats below.
How it works
Neo4j's neo4j/neo4j+s/neo4j+ssc schemes mean "routed" across the ecosystem,
in contrast to bolt/bolt+s/bolt+ssc, which mean "connect directly to this
one server". The distinction is carried on the wire by a single field in the
Bolt HELLO message: when a connection opts into routing, bolty sends
routing = %{"address" => "<host>:<port>"} in the HELLO extras (Bolt 4.1+).
Its presence tells the server "this is a routed connection, forward my
statements as needed"; its absence means "serve everything here directly".
bolty already sends the two fields the server routes on — :mode ("w"/"r")
and :bookmarks — through every RUN and BEGIN. SSR simply lets the server
act on them.
Enabling it
Routing is derived from the scheme, so most setups need nothing extra:
# Routed: writes go to a leader, reads to a read member — automatically.
{:ok, conn} =
Bolty.start_link(
scheme: "neo4j+s",
hostname: "cluster-member-1.example.com",
port: 7687,
auth: [username: "neo4j", password: System.fetch_env!("NEO4J_PASSWORD")]
)neo4j* schemes enable routing by default; bolt* schemes disable it. Override
with the explicit :routing option when you need to:
# A neo4j:// URI pointed at a single instance or a proxy — opt out of routing.
Bolty.start_link(uri: "neo4j://localhost:7687", routing: false, auth: [...])
# Force routing on a bolt:// scheme (unusual — prefer the neo4j:// scheme).
Bolty.start_link(scheme: "bolt", hostname: "member-1", routing: true, auth: [...])Precedence follows the rest of bolty's config: an explicit :routing option wins
over the scheme-derived default.
Choosing an access mode
Under routing, tag reads so they can be served by a read member instead of loading the leader:
# Routed to a writable member (default).
Bolty.query!(conn, "CREATE (:Person {name: $n})", %{n: "Ada"})
# Routed to a read member.
Bolty.query!(conn, "MATCH (p:Person) RETURN p", %{}, mode: "r"):mode and :bookmarks are documented on Bolty.query/4 and
Bolty.transaction/4.
Server prerequisites
- Neo4j Enterprise Edition running as a causal cluster. SSR does nothing on Community Edition or a standalone server (a routed connection still works — it just resolves to that one server).
- Routing must be enabled on the cluster members via
dbms.routing.enabled. Whether that is on by default, and howdbms.routing.default_routerinteracts with it, varies by Neo4j version — verify the defaults for your specific Enterprise version rather than assuming; don't take a value stated here or elsewhere as authoritative for your deployment.
Caveats
Read these before depending on SSR. They are consequences of bolty deliberately not implementing a full routing driver.
- You are pinned to one member. The
addressbolty advertises is the single member you configured. Writes and uncacheable reads that land on the wrong member cost an extra forwarding hop, and there is no read-replica load-balancing or scaling — every connection enters through the same member. - No automatic failover. If the configured member goes down, bolty does not discover another one; new connections to it fail. Front the cluster with DNS or an L4 (TCP) load balancer over the members and point bolty at that name — this composes cleanly with SSR, since routing is negotiated per connection once the TCP connection lands on any live member.
- Causal consistency is manual. There is no session abstraction, so
read-after-write consistency requires threading bookmarks yourself: read the
bookmarkfrom aBolty.Response(or the last response in a transaction) and pass it as:bookmarksto the next query/transaction. Without this, a read routed to a lagging member may not see a write you just made against the leader. - No topology awareness. bolty does not fetch or cache a routing table, so it cannot pick "the nearest read replica" or re-route around a demoted leader mid-connection the way the official drivers do.
If you need automatic failover, read-replica load-balancing, or topology-aware routing, put a load balancer in front (for failover/spread) and use bookmarks (for consistency) — or use an official Neo4j driver that implements full client-side routing.