The agent's response to the user is not blocked on Neo4j round-trips — and this example measures by how much.
This example shows the fire-and-forget write API in neo4j-agent-memory. With write_mode="buffered", calls to client.buffered.submit(...) queue Cypher writes and return immediately; a background drain task talks to Neo4j out-of-band. You call client.flush() at shutdown (or between bursts) and inspect client.write_errors for any background failures.
⚠️ Neo4j Labs ProjectThis example is part of
neo4j-agent-memory, a Neo4j Labs project. It is actively maintained but not officially supported. APIs may change. Community support is available via the Neo4j Community Forum.
Need a different LLM or embedding model? As of
neo4j-agent-memoryv0.3 you can swap providers via a single string —MemorySettings(llm="anthropic/claude-opus-5", embedding="BAAI/bge-small-en-v1.5"). See Bring Your Own Model.
MemorySettings.memory.write_mode = "buffered"— opt-in fire-and-forget mode.MemorySettings.memory.max_pending— bound the queue so sustained overload applies back-pressure instead of silently growing memory. The library default is 200; section 3 of the run drops it to 4 so you can seesubmit()block.client.buffered.submit(query, params)— enqueue a write and return immediately.client.buffered.pending— live queue depth.client.flush()— drain the queue (blocks until queued writes have committed).client.wait_for_pending()is an alias, andclose()/ leaving theasync withblock drains too.client.write_errors— background failures since startup, each aBufferedWriteErrorwith the originating Cypher, the exception and a timestamp.- Where the boundary is —
client.short_term.add_message(...)returns aMessage, so it commits inline; the derived writes nothing in the turn reads back (an audit node, a session counter) are what goes on the fire-and-forget channel.
When to reach for it: agent turns where the user-visible latency budget cannot absorb a write round-trip — typing-feel chat UIs, streaming token responses where a side-effectful write would otherwise stall the stream, ingestion pipelines where you want the producer decoupled from Neo4j throughput.
| File | Purpose |
|---|---|
main.py |
Runs the same sequential 50-turn agent loop in sync then buffered mode and prints both totals; then shows queue depth, back-pressure with a small max_pending, and a populated client.write_errors. |
- A dedicated empty AuraDB instance with its connection variables exported; follow Aura setup and cleanup.
neo4j-agent-memoryinstalled with thesentence-transformersextra (uv sync --extra sentence-transformers) — the demo uses a local embedder and no LLM, so no API key is needed.- Bolt only.
client.bufferedraisesNotSupportedErroron the hosted NAMS backend, which commits writes server-side;client.write_errorsis always empty there. See Bolt vs NAMS.
From the repo root:
uv run python examples/buffered-writes/main.pyYou should see something like:
1. Sync vs buffered — same loop, same writes, measured
sync: 271.6 ms for 50 turns (5.43 ms/turn)
buffered: 157.4 ms for 50 turns (3.15 ms/turn)
→ 1.7x faster on the user-visible path, 2.28 ms saved per turn
(each turn also does one inline add_message — identical in both modes)
2. Queue depth
pending when the last turn returned: 2 # varies: 0 = drainer kept up
pending after flush(): 0 # the invariant that matters
3. Back-pressure (50 submits, roomy queue vs. max_pending=4)
max_pending=200: 0.04 ms, queue peaked at 50
max_pending=4: 61.40 ms, queue peaked at 4
submit() blocks once the queue is full — bounded memory, no dropped writes
4. Error channel (one deliberately invalid write)
query: 'MERGE (t:AgentTurn {turn: $turn}) SET t.'
error: CypherSyntaxError
when: 2026-09-10T22:15:36.616932+00:00
submit() and flush() both returned normally — nothing raised
Verification
AgentTurn rows for the buffered session: 50
AgentTurn rows for the sync session: 50
Reading the numbers:
- The ratio in section 1 is machine- and network-dependent. Against a local Docker Neo4j each round-trip is only a couple of milliseconds, so the gap is small; against a remote database where a round-trip costs 20–50 ms, removing two derived writes per turn from the critical path matters far more. Each turn also performs one inline
add_messagethat is identical in both modes, which is why the buffered total is not near-zero. pendingin section 2 varies run to run — it is a liveqsize(). Only the post-flush()value is guaranteed, and it is always 0.- Section 4 also emits a
WARNINGfrom the library's drainer on stderr (Buffered write failed: … Error retained at client.write_errors[0]). Its interleaving with stdout varies; the failure is retained either way.
- How-to guide:
docs/modules/ROOT/pages/how-to/buffered-writes.adoc— backpressure semantics, error handling, when not to buffer (writes you'll read back immediately).
Historical verification report — 2026-09-10. The following records a prior checkout/test report. Its development-version labels, passing counts, and release-availability statements are historical, not evidence of current package compatibility.
Verified against
neo4j-agent-memoryv0.5.0 and Neo4j 5.26 (Docker) on 2026-09-10. The buffered-write API shipped in v0.2.0.