Build1 distinct publisher3 min readUpdated
An Elixir developer measured about 78 stale reads in 2000 put-then-get iterations against the same Zenoh key. The write path acknowledges locally; the read path goes to the router.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A developer writing up experiments with Zenoh through its Elixir bindings, Zenohex, ran 2000 iterations of put immediately followed by get on the same key, and about 78 of them, 3.9 percent in that run, read back the previous value [1][2][3]. That matters because the code under test was not doing pub/sub: it was using Zenoh's put/get as a small key-value store, which is exactly the usage that assumes a write is readable once the write call returns [1].
It is not data loss. According to the author, an extra get issued immediately afterwards almost always returns the correct value, and the fastest confirmation measured was a single additional get about 1 millisecond later [4]. The value arrives; it just is not visible yet when the caller is already asking for it.
The mechanism is visible in the binding. Zenohex.Session.put/4 is a thin Rustler wrapper over zenoh-rust's put, and the NIF's .wait() only waits for the local session to finish queueing the message, not for the zenohd router backing the storage to receive and apply it [5]. The read side is different in kind: session_get is registered as a DirtyIo NIF and genuinely blocks for a reply from the remote side within a timeout, a real request/response [6]. The author's framing is the one to keep: put behaves like a GenServer cast, get behaves like a call, and firing a cast then immediately making a call that depends on it is the classic shape of this race [7].
This is not a binding defect being blamed upstream. Eclipse Zenoh has an open design issue, #2511, titled "[Design] Acknowledged put: confirmed storage writes via query path vs protocol extension", still open as of the writeup [8]. The issue states the position plainly: the pub/sub path is fire-and-forget, and session.put() returns when the message is sent, not when it is stored [9].
Do the arithmetic on what a few percent means in an operational loop. At 3.9 percent, roughly one write in 26 is unreadable at the moment the next line of code looks for it [13]. In a service that writes state and then reads it back to make a decision, that is not an edge case you will find in a smoke test and will find in production logs, if you are comparing values at all. The failure is silent: the get succeeds, the reply is well-formed, the payload is one generation old.
The author's workaround is the honest one available today: a wrapper that puts, then polls get on the same key until the written payload reads back, retrying at a short interval until a deadline, returning :ok on confirmation and {:error, :not_confirmed} if it never lands [10]. Defaults are a 3000 ms confirm timeout, a 1 ms confirm interval, and a 3000 ms query timeout [11]. The price is at least one extra round trip per write, plus a poll loop on the unlucky percent [15].
Two things to watch. Whether #2511 lands as a protocol-level acknowledgement or as a blessed query-path confirmation decides whether every Zenoh client library needs its own version of this wrapper [8]. And measure your own rate before trusting 3.9 percent: that number is one run on one setup as reported in the post [3], and the post is an AI translation of the author's original Japanese article on Qiita [12].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The author's workaround, ZenohAckPut.put/5, calls Zenohex.Session.put and then polls get on the same key, returning :ok only once the written payload reads back, retrying at a short interval until a timeout, and returning {:error, :not_confirmed} if confirmation never lands.
ZenohAckPut defaults are a 3000 ms confirm timeout, a 1 ms confirm interval, and a 3000 ms query timeout.
Out of 2000 iterations, a small fraction printed "stale!": about 78 (3.9%) in one run.
The author experimented with Zenoh via its Elixir bindings, Zenohex, using the put/get storage feature rather than the usual pub/sub use case, and found that every so often the state read back was one step behind.
The reduced test loops 2000 iterations of Zenohex.Session.put followed immediately by Zenohex.Session.get on the same key, with a 3000 ms timeout and consolidation: :latest, comparing the returned payload to the one just written.
Querying again immediately afterwards almost always returns the correct value; the fastest the author measured was a single extra get about 1 ms later. The value does not disappear; there is a small window of lag before the write is visible.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Reproducible but single-observer
The mechanism is unusually well grounded for a blog post: a runnable reproduction loop, the actual NIF source showing .wait() only queues the local publish, a contrasting DirtyIo get, and a quoted upstream issue confirming put is fire-and-forget. What holds the score down is that everything comes from one first-party author on one publisher, the 3.9% figure is from a single run with no stated Zenoh version, router topology, hardware or load, and no independent party has replicated either the failure or the wrapper's clean result.
One practitioner, unpackaged fix
Adoption signals are minimal and all trace to the same author: a personal experiment using Zenoh put/get for state handoff, benchmark runs, and a mitigation module published to GitHub but not to Hex, consumable only as a git dependency. The upstream issue shows the problem is recognized by the Zenoh project but not that anyone has shipped or deployed a fix. Nothing in the supplied material shows other users, downstream projects, or production deployments.
Close to evidence, mildly generalized
The writeup is hedged where it matters — 'in one run', 'a few percent of the time in my measurements', 'it's not that the value disappears' — and its central mechanism is backed by source code plus an upstream quote, so overstatement is limited. The small positive gap reflects presenting a rate from a single uncharacterized environment as a property of Zenoh put/get, and reporting the wrapper as a clean fix without any latency or throughput cost accounting even though every confirmed write adds at least one round trip.
Mild self-promotion, no commercial stake
Observable incentives are weak: an individual developer publishing a debugging narrative on a general developer platform, with the mitigation being his own unmonetized GitHub module. That creates a mild interest in the problem looking real and the wrapper looking sufficient. No vendor sponsorship, employer positioning, competing-product comparison, or paid placement is visible in the supplied material, and the author credits the upstream project's own issue rather than attacking it.
Mechanism solid, magnitude tentative
High confidence that the described asymmetry is real and that read-your-own-writes is not guaranteed on Zenoh's put/get path: the NIF code and the open upstream issue agree. Lower confidence in the specific 3.9% rate, in how it generalizes across Zenoh versions and deployment topologies, and in the wrapper's cost profile under load, all of which rest on one author's single run in the only source available.
build
The Elixir arbitrage roadmap that puts the Rust parser third, not first1 distinct publisher
build
A 404 for a live article shows why a duplicate check can never prove absence1 distinct publisher
build
A supervisor rebuilt from spawn_link and receive shows where restart bugs actually live1 distinct publisher
build
Force the tool call, then hand Lightsail a long-lived key1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026