Memcache
← Back to Changelog

v1.12.0

Release

memcache@1.12.0 — 2026-10-06

Add maxPendingCommands, fix connection and timeout behavior, and speed up commands.

Features

  • add maxPendingCommands to fail requests fast under overload (#162)

    const client = new Memcache({
      nodes: ['localhost:11211'],
      maxPendingCommands: 1000, // default 0: no limit
    });
    

    While a node has that many requests waiting for a response or for its connection to open, a new request to it fails at once instead of waiting: set() resolves false and get() resolves undefined on an open connection; while the connection is being opened, single-key commands reject. Refused requests are not retried, and quit() still goes out. MemcacheNode gains connectForRequest(), which the client's commands use to wait for a connection.

Bug Fixes

  • give concurrent binary/SASL requests their own responses: three concurrent binaryGet()s could all return the first key's value (#150)
  • open one connection per node and scope socket handlers to their socket: 50 concurrent requests on a cold client opened 50 sockets, and a replaced socket could close or feed data into the live one (#151)
  • time out commands the server doesn't answer, and keep idle connections open (#152)
  • fix a crash when a hit or miss listener closes the connection, such as client.on("hit", () => client.disconnect()) (#160)
  • give the site favicon a transparent background (#146)

Performance

  • coalesce socket writes per tick: one write per batch of pipelined commands instead of one per command (#153)
  • find multi-get misses in linear time (#154)
  • buffer large values without re-copying them on every chunk (#155)
  • take commands off the queue in O(1) (#156)
  • write large storage values (64 KB and up) without copying them (#157)
  • look up a key's node without allocating, and skip hashing with one node (#158)
  • send commands without extra async layers (#159)
  • parse replies without slicing or decoding each line (#160)
  • build each binary packet in one allocation (#161)

The same benchmark files run against 1.11.0 and this branch in one session, taking turns (time per operation, so lower is better):

Benchmark1.11.01.12.0Speedup
500 gets, 100 in flight5.8 ms1.9 ms3.1×
500 sets, 100 in flight5.0 ms1.6 ms3.2×
500 gets, 10 in flight7.6 ms5.4 ms1.4×
500 gets, 1 in flight41 ms40 msunchanged (one round trip at a time)
gets() of 10,000 keys, one batch1.69 s22 ms77×
gets() of 10,000 keys, 10 batches228 ms19 ms12×
60,000 gets in one burst6.7 s296 ms23×
get of a 4 MB value, TLS105 ms10 ms10.6×
get of a 4 MB value, TCP31 ms8.5 ms3.6×
4 sets of 1 MB values, TCP8.5 ms6.2 ms1.4×
1,000 tasks setting, getting and deleting 10 keys, one at a time519 ms412 ms1.26×

In the README's comparison with memjs and memcached, 1.12.0 is now the fastest client in every row. In the same session, 1.11.0 trailed memjs on gets with 100 or more in flight, on sets with 500, and on multi-gets of 1,000 or more keys per batch.

Behavior changes

  • timeout (#152) is now a connect timeout plus a deadline for pending commands, not an idle timer. Idle connections stay open, where before they were dropped after timeout. When pending commands get no response within timeout, the node emits timeout, fails them and closes the connection, even while the client keeps writing; the next command reconnects. Setting client.timeout also applies to commands already waiting.
  • node.commandQueue (#156) returns a copy, so changing it no longer changes the queue.
  • getNodesByKey() with ModulaHash (#158) returns frozen arrays, as KetamaHash already did, so mutating the result throws.
  • execute() and node.command() (#159) are no longer async functions. They return the same promises, and on a connected node without hooks a command is written before the call returns.
  • Multi-get hit and miss events (#160) fire after the multi-get's promise is settled. They still fire before code awaiting it continues.
  • CommandOptions.data (#157) is new: a storage command can pass its value separately from the command line.

Documentation

  • add the September 2026 hardening and performance plan (#147)
  • regenerate the README benchmark tables in one session (#163)

Internal

  • run flush tests against a dedicated memcached server (#148)
  • add a performance benchmark suite: set/get/delete against memjs and memcached, concurrency, multi-get, large values, bursts and cold start (#149)
  • sync defense-in-depth files from agentic (#164)
  • upgrade Biome to 2.5.14 (#165)
  • upgrade Vitest to 5.0.2 (#166)
  • keep the why-is-node-running override (#167)
  • upgrade TypeScript and build tooling (#168)
  • upgrade pnpm to 12.8.1 (#169)
  • upgrade GitHub Actions (#170)
  • upgrade the Dev Container image (#171)
  • upgrade docula to 3.0.1 (#172)
  • upgrade tinybench to 6.2.0 (#173)
  • upgrade wrangler to 4.143.0 (#174)
  • upgrade hashery to 3.0.2 (#175)
  • upgrade hookified to 3.0.4 (#176)
  • expand CODEOWNERS paths (#177)
  • bootstrap Safe Chain for Claude Code (#178)
  • use kebab-case workflow names (#179)
  • record the workflow-names merge (#180)
  • record lockdown and cloud setup (#181)

Contributors

  • @claude (55)
  • @jaredwray (19)

Full List of Changes

  • fix: give the site favicon a transparent background by @jaredwray in #146
  • docs: add September 2026 hardening and performance plan by @jaredwray in #147
  • test: run flush tests against a dedicated memcached server by @jaredwray in #148
  • chore: add a performance benchmark suite by @jaredwray in #149
  • fix: give concurrent binary/SASL requests their own responses by @jaredwray in #150
  • fix: open one connection per node and scope socket handlers by @jaredwray in #151
  • fix: time out commands the server doesn't answer, keep idle connections by @jaredwray in #152
  • perf: coalesce socket writes per tick by @jaredwray in #153
  • perf: find multi-get misses in linear time; bench: compare with memjs and memcached by @jaredwray in #154
  • perf: buffer large values without re-copying; bench: random-order client comparison and README refresh by @jaredwray in #155
  • perf: take commands off the queue in O(1) by @jaredwray in #156
  • perf: write large storage values without copying them by @jaredwray in #157
  • perf: look up a key's node without allocating, and skip hashing with one node by @jaredwray in #158
  • perf: send commands without extra async layers by @jaredwray in #159
  • perf: parse replies without slicing or decoding each line, and fix a listener crash by @jaredwray in #160
  • perf: build each binary packet in one allocation by @jaredwray in #161
  • feat: add maxPendingCommands to fail requests fast under overload by @jaredwray in #162
  • docs: regenerate the README benchmark tables and draft the 1.12.0 release notes by @jaredwray in #163
  • mono - chore: sync defense-in-depth files from agentic by @jaredwray in #164
  • mono - chore: upgrade code quality dependencies by @jaredwray in #165
  • mono - chore: upgrade Vitest by @jaredwray in #166
  • mono - chore: keep why-is-node-running override by @jaredwray in #167
  • mono - chore: upgrade TypeScript and build tooling by @jaredwray in #168
  • mono - chore: upgrade pnpm by @jaredwray in #169
  • mono - chore: upgrade GitHub Actions by @jaredwray in #170
  • mono - chore: upgrade Dev Container images by @jaredwray in #171
  • mono - chore: upgrade docula by @jaredwray in #172
  • mono - chore: upgrade tinybench by @jaredwray in #173
  • mono - chore: upgrade wrangler by @jaredwray in #174
  • mono - chore: upgrade hashery by @jaredwray in #175
  • mono - chore: upgrade hookified by @jaredwray in #176
  • mono - chore: defense - expand CODEOWNERS paths by @jaredwray in #177
  • mono - chore: defense - bootstrap Safe Chain for Claude Code by @jaredwray in #178
  • mono - chore: defense - use kebab-case workflow names by @jaredwray in #179
  • mono - chore: defense - record workflow-names merge by @jaredwray in #180
  • mono - chore: defense - record lockdown and cloud setup by @jaredwray in #181

Full diff: https://github.com/jaredwray/memcache/compare/v1.11.0...v1.12.0