Transactions, Lua & Tooling: Overview
Verified on Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24, Apple-silicon Mac. The script’s output is from my run.
What this module covers
Section titled “What this module covers”Redis goes beyond simple key-value operations. As an application grows you need to run several commands without interference, move logic next to the data, and diagnose the server when it slows down.
| Lesson | Topic |
|---|---|
| Transactions | MULTI/EXEC/DISCARD, EXECABORT, no rollback, optimistic locking with WATCH |
| Pipelines | Batching round trips (5000 SETs in 17 ms vs 1688 ms), redis-cli --pipe, result tuples |
| Lua Scripting | EVAL, redis.call/pcall, the server-side script cache, SCRIPT KILL, Functions |
| Tooling & Monitoring | INFO, SLOWLOG, --bigkeys/--keystats, MEMORY, LATENCY, MONITOR cost |
Why grouping commands matters
Section titled “Why grouping commands matters”Command execution is single-threaded; the process is not. Redis runs one command at a time, so a single command never interleaves with another. Your application, though, usually needs several commands, and between two of them another client can run. There are three tools, and each solves a different problem:
- Transactions (
MULTI/EXEC) queue a batch and run it as one uninterrupted block. No branching on intermediate results and no rollback. - Pipelines send many commands in one network round trip. Speed, not atomicity, and per-command errors come back inside the reply array.
- Lua scripts run entirely on the server, atomically, with branching and the full command set. They block the server while they run.
Why it matters: 20 buyers, 5 items
Section titled “Why it matters: 20 buyers, 5 items”The script below sells 5 items to 20 concurrent buyers twice. The first version checks the stock, then decrements in a pipeline (a common belief is that a pipeline “makes it atomic”). The second does the check and the decrement inside one Lua script.
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);const BUYERS = 20;const STOCK = 5;
// "check, then decrement" as two steps. Wrapping the second step in a pipeline does not help:// the check already happened, and other buyers ran their checks before any decrement arrived.async function buyPipelined() { const stock = Number(await redis.get('b3:tsl:stock')); if (stock > 0) await redis.pipeline().decr('b3:tsl:stock').exec();}
// the same decision inside one Lua script: read, compare and write cannot be interleavedredis.defineCommand('buyLua', { numberOfKeys: 1, lua: `local s = tonumber(redis.call('GET', KEYS[1])) if s and s > 0 then redis.call('DECR', KEYS[1]) return 1 end return 0`,});declare module 'ioredis' { interface RedisCommander<Context> { buyLua(key: string): Promise<number>; }}
await redis.set('b3:tsl:stock', String(STOCK));await Promise.all(Array.from({ length: BUYERS }, buyPipelined));console.log(`GET then pipelined DECR : ${BUYERS} buyers, stock ${STOCK} -> stock left ${await redis.get('b3:tsl:stock')}`);
await redis.set('b3:tsl:stock', String(STOCK));const sold = (await Promise.all(Array.from({ length: BUYERS }, () => redis.buyLua('b3:tsl:stock')))).reduce((a, b) => a + b, 0);console.log(`Lua check-and-decrement : ${BUYERS} buyers, stock ${STOCK} -> sold ${sold}, stock left ${await redis.get('b3:tsl:stock')}`);
await redis.del('b3:tsl:stock');await redis.quit();Real output (this Mac):
GET then pipelined DECR : 20 buyers, stock 5 -> stock left -15Lua check-and-decrement : 20 buyers, stock 5 -> sold 5, stock left 0The pipelined version sold 20 items it did not have (stock left -15): all 20 buyers passed the stock > 0 check before any decrement arrived. The Lua script sold exactly 5. MULTI/WATCH would also give exactly 5, at the cost of retries (Transactions).
Why tooling matters
Section titled “Why tooling matters”A correct application can still suffer slow queries, oversized keys or connection leaks. Redis ships diagnostics that are safe to leave on (INFO, SLOWLOG, --keystats) and probes that are not (KEYS, MONITOR). The tooling lesson shows real output and measures the cost of the risky ones.
PING
INFO serverReading AI-generated code
Section titled “Reading AI-generated code”An assistant is asked to make “check stock then decrement” safe and reaches for a pipeline:
🐛 Spot the bug
async function buy() {const stock = Number(await redis.get('stock'));if (stock > 0) { await redis.pipeline().decr('stock').exec(); // "the pipeline makes the decrement atomic"}}Show the answer
A pipeline only batches the network I/O of the commands inside it. The GET and the check happen before it, so 20 concurrent buyers all saw stock = 5, all passed stock > 0, and all decremented: stock ended at -15. Put the read, the comparison and the write in one atomic unit, here a Lua script (or WATCH + MULTI with a retry loop, or a plain DECR followed by a check-and-undo):
redis.defineCommand('buy', { numberOfKeys: 1, lua: `local s = tonumber(redis.call('GET', KEYS[1])) if s and s > 0 then redis.call('DECR', KEYS[1]) return 1 end return 0`,});Mental Model
Section titled “Mental Model”Think of three ways to hand work to a single fast clerk. A pipeline is a stack of slips handed over in one trip: quick, but the clerk may serve someone else between your slips. A transaction is a sealed envelope: its slips are processed together, but the clerk cannot read the answer of one slip to decide the next. A script is a written procedure the clerk follows with the counter closed: it can branch, and it is atomic, but everyone waits while the clerk follows it.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| Pipeline | Fewest round trips, highest throughput | No atomicity; errors hide in the reply array |
MULTI/EXEC | Back-to-back execution | No branching, no rollback |
WATCH + MULTI | Check-and-set without a lock | A retry loop and a connection per attempt |
| Lua script / Function | Atomic branching logic | Blocks the server while running; must stay short |
| Built-in diagnostics | No extra software | The risky probes (KEYS, MONITOR) have a measurable cost |
Common Misconceptions
Section titled “Common Misconceptions”- “A pipeline is a transaction.” It only batches round trips; the oversell run above ended at -15.
- “Redis is single-threaded, so my multi-step logic is atomic.” Only each command (or transaction, or script) is.
- “Lua scripts are free.” They block every other client while they run.
- “Anything in
redis-cliis safe on production.”KEYSandMONITORare not (measured in the tooling lesson).