You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@upstash/redis sends each Redis command as a JSON POST to the Upstash REST API. Two default-on behaviors break data-integrity expectations the same library does honor via its more explicit API surface:
Retry double-apply. On any transport-level failure the client re-issues the same command (default retry: 5 attempts). For a non-idempotent command (INCR, LPUSH, SETEX, ...) whose effect the server already committed but whose response was lost, the retry applies the operation a second time. One redis.incr("counter") -> the counter is incremented twice.
Auto-pipelining ordering loss. Default enableAutoPipelining: true routes concurrent reads and writes to separate pipelines -> separate HTTP requests. A same-tick Promise.all([redis.set(k,v), redis.get(k)]) has no client-enforced ordering: the server can serve the read before the write, returning a stale value. An explicit redis.pipeline().set(k,v).get(k).exec() sends one ordered request and the read sees the write.
Both are default configuration (no opt-in required) and both are avoidable (retry: false; explicit pipeline), but neither avoidance is documented on the affected command methods.
Finding 1 -- retry loop
pkg/http.ts, request() (lines 261-290):
for(leti=0;i<=this.retry.attempts;i++){// default attempts = 5 -> 6 fetchestry{res=awaitfetch(requestUrl,requestOptions);break;}catch(error_){if(i<this.retry.attempts){awaitnewPromise((r)=>setTimeout(r,this.retry.backoff(i)));}// loop re-issues the SAME requestUrl + requestOptions (same body)}}
No idempotency key, no server-side dedup handle. "Response never arrived" includes the case where the server already applied the command and the connection died before the reply -- routine in serverless/edge networks, Upstash's primary deployment target.
PoC: mock endpoint commits an INCR then destroys the socket before responding (first hit only); retry succeeds.
$ node poc-retry-double-apply.mjs
client resolved value : 2
server applied INCR : 2 time(s) -> ["INCR counter","INCR counter"]
BUG CONFIRMED: one redis.incr() incremented the counter 2 times.
Finding 2 -- auto-pipelining ordering
pkg/auto-pipeline.ts classifies each command read/write (READ_COMMANDS set) and batches into two independent pipelines, each its own HTTP request:
A set and get in the same tick go out as two separate requests with no ordering between them. Contrast pkg/pipeline.tsexec(): one request, server-applied in order.
PoC:
== A) auto-pipelining (DEFAULT) Promise.all([set(k,"v1"), get(k)]) ==
HTTP requests seen by server : 2
A) get(k) returned: "old" <-- STALE: read served before write
== B) explicit pipeline redis.pipeline().set(k,"v1").get(k).exec() ==
HTTP requests seen by server : 1
B) get(k) returned: "v1" <-- ordered: read sees the write
Impact
Both are default-configuration, silent data-integrity defects, no error surfaced:
Finding 1: a counter increments twice, a list item pushed twice, on a routine transport fault.
Finding 2: any code doing a set + concurrent get on the same key can observe a stale read under default config while identical logic through an explicit pipeline is correct -- intermittent, hard-to-reproduce class of bug.
Suggested fix
Finding 1: idempotency mechanism (server-assigned request id + dedup window), or document per-command that the default is at-least-once, not exactly-once.
Finding 2: make auto-pipelining preserve read-after-write ordering for the same key, or document plainly that enableAutoPipelining does not guarantee read-your-writes.
Disclosure note
No SECURITY.md / private vulnerability reporting found on this repo, so filing as a public issue.
This report was produced with AI assistance (Claude, Anthropic), with two live PoCs run against the published @upstash/redis@1.38.4 package (Node 22) against a faithful in-process mock of the Upstash REST protocol, output captured verbatim above.
Activity