Skip to main content

Read replicas

Configure read-only connections beside the primary and MikroORM's connectionType option routes individual reads to them.

const orm = await MikroORM.init({
clientUrl: 'bolt://primary:7687',
user: 'neo4j',
password: 'password',
replicas: [
{ clientUrl: 'bolt://replica-1:7687', user: 'neo4j', password: 'password' },
{ clientUrl: 'bolt://replica-2:7687', user: 'neo4j', password: 'password' },
],
});

connectionType is honoured on every read — find, findOne, count, virtual entities, streams, and the relation loads behind populate:

const books = await em.find(Book, {}, { connectionType: 'read' });
const total = await em.count(Book, {}, { connectionType: 'read' });
const cursor = em.stream(Book, { connectionType: 'read' });

Three rules​

  • Absence means the primary. Replica reads are opt-in per query, never a default. A replica lags by an unbounded amount, and turning every find eventually-consistent on a caller's behalf is not a trade the driver gets to make.
  • A populated read stays on one connection. Root and relations go to the same place, so one logical read is not spread across two servers with different lag.
  • A transaction wins outright. Inside ctx the session belongs to the connection the transaction was opened on. Honouring the option there would run the statement outside the transaction — unable to see its uncommitted writes, and untouched by its rollback — so within a transaction the option is not so much overridden as unrepresentable.

Writes never consult it; core does not declare it on a write's options at all.

Connection selection never reaches the statement. The same find compiles to identical Cypher and identical parameters whichever connection it is sent on — the route is a property of sending a query, not of building one.

Manual control​

const readConn = em.getDriver().getConnection('read');
const writeConn = em.getDriver().getConnection('write');

// The live `neo4j-driver` instance behind either one.
const client = await writeConn.getClient();

getClient() is the escape hatch to the Bolt driver itself — the same shape the other MikroORM drivers expose as getKnex() / getClient(). It is how a library outside the ORM shares this connection instead of opening a second pool; serving a generated GraphQL API is the case it exists for. The connection owns the driver, so orm.close() closes it and you should not.

Access mode​

A read-only transaction opens its session in read access mode; anything else opens in write mode. On a cluster the routing driver can send a read-mode session to a follower, and a stray write inside one is refused by the server — surfacing as MikroORM's ReadOnlyException rather than a raw Neo4j error.

await em.transactional(async (em) => em.find(Book, {}), { readOnly: true });

Absence means write, which is the safe direction to guess. A declared routine is refused earlier still — before a statement is sent — because its declaration says what access it needs.

Replica reads can be stale

That is the whole trade you are making by asking for one. Read-after-write within a single request is the case that bites; leave those on the primary.