Let’s look at a real scene first: 1 million Redis keys need to be processed, CPU intensive + I/O intensive coexist, how to solve it? Using Bun.redis + Worker, a 4-core machine achieves 4x acceleration, and a single thread can support 40,000 QPS.
⚠️ Environmental requirements: Bun ≥ 1.3.14, Redis ≥ 6.0(Bun.redis uses RESP3 protocol handshake by default and requires Redis 6+
HELLOcommand support). Redis 5 and below reportERR unknown command 'HELLO'.
"Squeeze" experiment with 1 million keys
Suppose you have 1 million user portraits (Hash structure) in Redis, and now you need to do it onceFull data processing:
// each user is a Hash, have name, age, tags Wait for dozens of fields
// Task: take out → parse → Encrypt sensitive fields → write back
Naive version (single-threaded serial) - don’t even think about running to the end
const redis = new Bun.RedisClient("redis://localhost:6379");
const keys = await redis.keys("user:*"); // 100 Ten thousand
for (const key of keys) {
const data = await redis.hgetall(key); // I/O
const encrypted = encrypt(JSON.stringify(data)); // CPU
await redis.hset(key, { data: encrypted }); // I/O
}
// Estimated time: 3-5 Hour
// event loop 100% block, HTTP The service is directly stuck
Advanced version: Automatic Pipeline (without Worker) - I/O is up, but the CPU is still the bottleneck
// put all first key Take it out at once (automatic pipeline, 1 Second-rate RTT)
const pipeline = [];
for (let i = 0; i < 1000; i++) pipeline.push(redis.hgetall(`user:${i}`));
const datas = await Promise.all(pipeline); // 1ms get
// but encrypt() Run on main thread
for (const data of datas) {
encrypt(JSON.stringify(data)); // stuck event loop
}
// Estimated time: 40-60 minute(I/O quick, CPU slow)
// encryption 10 Ten thousand times, HTTP P99 soar 5 Second
Ultimate version: Worker + Bun.redis - 4 cores fully loaded
// main.ts
// ⚠️ Worker yes Bun global object, no need import
const redis = new Bun.RedisClient("redis://localhost:6379");
const keys = await redis.keys("user:*");
const chunks = chunkArray(keys, 4); // point 4 share
const workers = chunks.map(() => new Worker(new URL("./encrypt-worker.ts", import.meta.url)));
const results = await Promise.all(
workers.map((w, i) => {
w.postMessage(chunks[i]);
return new Promise((resolve) => (w.onmessage = (e) => resolve(e.data)));
})
);
console.log(`Processing completed: ${results.reduce((a, b) => a + b, 0)} strip`);
// Estimated time: 10-15 minute(4 Double acceleration)
// The entire event loop is smooth, HTTP Service is not affected
// encrypt-worker.ts
// ✅ Bun.RedisClient It’s the overall situation; it can also be import { RedisClient } from "bun"
declare var self: Worker;
self.onmessage = async (e) => {
const redis = new Bun.RedisClient("redis://localhost:6379");
let count = 0;
// Run in independent thread CPU Intensive logic, does not block the main event loop
for (const key of e.data) {
const data = await redis.hgetall(key);
const encrypted = encrypt(JSON.stringify(data));
await redis.send("HSET", [key, "data", encrypted]); // see below Streams Same style send
count++;
}
self.postMessage(count);
};
| version | Time consuming | CPU utilization | event loop blocking | | :-- | :-- | :-- | :-- | | Naive serial | 3-5 hours | 25% (single core) | completely stuck | | Automatic Pipeline (single thread) | 40-60 minutes | 25% (single core) | stuck during encryption | | Worker + Bun.redis (4 cores) | 10-15 minutes | 100% (4 cores fully loaded) | completely unobstructed |
This is the real strength of Bun.redis: not "a particular item is particularly strong", but "it allows you to do the three things of multi-threading + high concurrency + automatic pipeline with the least amount of code".
Let’s break it down one by one.
1. Panoramic view of strengths
|
Dimensions
|
Bun.redis
|
ioredis/node-redis
|
| :-- | :-- | :-- |
| Install |
✅ Zero dependencies, built-in
|
❌ Requires npm i, version dependency hell
|
| performance |
⭐⭐⭐⭐⭐ Zig/Rust Kernel
|
⭐⭐⭐⭐ JS + libuv
|
| Pipeline |
✅ Automatically enabled
|
❌ Requires manual operation .pipeline()
|
| TypeScript |
✅ Native type
|
⚠️ Required @types/ioredis
|
| API design |
fetch / Promise style
|
Chained .then()
|
| cold start |
almost zero overhead
|
2-5ms more to resolve dependencies
|
| Cluster/Pub-Sub/Stream |
✅ Full support
|
✅Supported but requires additional configuration
|
| Multithreading (Worker) |
✅ postMessage is 400 times faster
|
⚠️ Serialization in hundreds of microseconds
|
One sentence summary:You write Bun code, Bun.redis is like fetch, Bun.file Just as natural, no need to think about "which bag to put it in".
2. Strength 1: Zero dependency, native integration
In the Node.js ecosystem, the Redis client is a "heavy asset":
# Node.js
npm install ioredis # Still have to pretend @types/ioredis
# version conflict?TypeScript Report an error? Lock file explosion?
Bun gives you directly:
// Bun - Zero dependencies
const redis = new Bun.RedisClient("redis://localhost:6379");
await redis.set("key", "value");
No node_modules, no version conflicts, no Cannot find module 'ioredis'. This has huge benefits in serverless, edge computing, and Docker images - the image is small, the cold start is fast, and the dependencies are clean.
3. Strength 2: Zig/Rust kernel, crushing performance
Bun.redis used Zig/Rust Directly implement RESP protocol parsing,Bypass libuv, direct system call.
Official benchmark (GET/SET scenario)
| client | ops/sec | Delay P99 | Memory usage | | :-- | :-- | :-- | :-- | | Bun.redis | ~280,000 | 0.3ms | benchmark | | ioredis | ~190,000 | 0.6ms | +40% | | node-redis | ~170,000 | 0.8ms | +50% |
Key points:
-
• Protocol parsing runs in native code,Does not consume JS thread time
-
• For TCP socket
epoll/kqueue/io_uringdirect management -
• Zero-copy buffer delivery,What the JS side gets is TypedArray and is not copied.
Stress test code
import { Bench } from "tinybench";
const bench = new Bench({ time: 5000 });
const redis = new Bun.RedisClient("redis://localhost:6379");
bench
.add("Bun.redis SET", async () => {
await redis.set(`k:${Math.random()}`, "value");
})
.add("Bun.redis GET", async () => {
await redis.get("k:0.5");
});
await bench.run();
console.table(bench.table());
4. Strength 3: Automatic Pipeline, the strongest hidden feature
This is Bun.redis most underratedStrengths——It defaults to pipeline,Need not .pipeline() No need .multi().
1000 requests compared
ioredis (requires manual pipeline):
const redis = new Redis();
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
pipeline.get(`key:${i}`);
}
const results = await pipeline.exec();
Bun.redis (automatic pipeline):
const redis = new Bun.RedisClient("redis://localhost:6379");
const promises = [];
for (let i = 0; i < 1000; i++) {
promises.push(redis.get(`key:${i}`));
}
const results = await Promise.all(promises);
What's going on underneath?
Performance gap:
| mode | Number of network round trips | Time consuming | | :-- | :-- | :-- | | serial await (no pipeline) | 1000 times | ~3000ms | | ioredis manual pipeline | 1 time | ~3ms | | Bun.redis automatic pipeline | 1 time | ~2ms |
1000x acceleration, that’s it.
5. Strength 4: TypeScript native first-class citizen
Need not @types/xxx,Need not declare module, type inference is top-level:
const redis = new Bun.RedisClient("redis://localhost:6379");
// Full type hint
// The object will be passed directly TS Report an error + runtime TypeError, Not secretly toString
await redis.set("user:1", "Alice"); // must be string | number | Buffer
const result = await redis.get("user:1");
// result: string | null ← Tell you clearly that it may be null
// batch mget: Note that it is a variable length parameter (not an array))
const users = await redis.mget("user:1", "user:2", "user:3");
// users: (string | null)[]
// Hash operation(hset Supports multiple forms: single field, variable length pair, object)
await redis.hset("user:1", { name: "Alice", age: "30" });
// Equivalent to:
// await redis.hmset("user:1", ["name", "Alice", "age", "30"]);
// await redis.hset("user:1", "name", "Alice", "age", "30");
const user = await redis.hgetall("user:1");
// user: { name: string, age: string, ... }(Actually it is with null prototype object)
Chained APIs also have complete types:
// ZADD Returns the number of elements added
const added: number = await redis.zadd("scores", 100, "alice", 200, "bob");
// ZRANGEBYSCORE Return array
const top: string[] = await redis.zrangebyscore("scores", 0, 1000);
6. Strength 5: Modern API design
The API of Bun.redis is borrowed from fetch / Promise The modern style is much cleaner than the chain call of ioredis.
Connection management
// Simple connection
const redis = new Bun.RedisClient("redis://localhost:6379");
// URL Carry certification
const redis2 = new Bun.RedisClient("redis://:password@host:6379/0");
// TLS + cluster
const redis3 = new Bun.RedisClient("rediss://cluster.example.com:6380", {
tls: { rejectUnauthorized: true },
});
// With connection options (note: no reconnect field, yes autoReconnect + maxRetries)
const redis4 = new Bun.RedisClient("redis://localhost:6379", {
connectionTimeout: 5000, // Connection timeout (milliseconds)
idleTimeout: 30000, // idle timeout
autoReconnect: true, // Automatically reconnect (default true)
maxRetries: 10, // Maximum number of retries
enableAutoPipelining: true, // automatic Pipeline(default true)
});
Pub/Sub (native support)
const sub = new Bun.RedisClient("redis://localhost:6379");
await sub.subscribe("news", (message, channel) => {
console.log(`[${channel}] ${message}`);
});
// release
const pub = new Bun.RedisClient("redis://localhost:6379");
await pub.publish("news", "Hello world");
Streams (event streams)
const redis = new Bun.RedisClient("redis://localhost:6379");
// ⚠️ Bun.redis No xadd/xread Convenient method, use send Take the original agreement
// producer
const id = await redis.send("XADD", ["events", "*", "type", "click", "user", "alice"]);
// consumer
const stream = await redis.send("XREAD", ["COUNT", "10", "STREAMS", "events", "0"]);
console.log(stream);
Transaction/Lua Script
// ⚠️ Bun.redis No multi()/eval() Convenient method, use send
// MULTI/EXEC
await redis.send("MULTI", []);
await redis.send("SET", ["a", "1"]);
await redis.send("INCR", ["b"]);
const results = await redis.send("EXEC", []); // ["OK", 1]
console.log(results);
// EVAL
const result = await redis.send(
"EVAL",
["return redis.call('GET', KEYS[1])", "1", "mykey"]
);
7. Strength 6: Automatic reconnection + cluster awareness
Automatic reconnection (out of the box)
const redis = new Bun.RedisClient("redis://localhost:6379", {
// ⚠️ The option name is autoReconnect / maxRetries, no reconnect
autoReconnect: true, // Automatic reconnection after disconnection, no need to write retry logic yourself
maxRetries: 10, // Maximum retries 10 times (default)
// For internal use 50ms Starting, exponential backoff for each doubling, and capping 2 Second
});
// You just care await, Automatically continue after disconnection and recovery
const value = await redis.get("key");
// You can also listen to connection events
redis.onconnect = () => console.log("connected");
redis.onclose = (err) => console.log("disconnected:", err);
console.log(redis.connected); // boolean
console.log(redis.bufferedAmount); // Current number of buffered bytes
cluster mode
// Automatic sharding of multiple nodes (use one client to connect multiple nodes)
const cluster = new Bun.RedisClient([
"redis://node1:6379",
"redis://node2:6379",
"redis://node3:6379",
]);
// Internal automatic calculation slot, Routing requests, handling node failures
await cluster.set("user:1", "Alice");
const user = await cluster.get("user:1");
8. Strength 7: Worker multi-threading, 400 times faster than Node.js
If you really need"Compute + Redis"A balanced multi-threading solution, Bun's Worker is the "fastest seam".
Basic usage (return to the example at the beginning)
// main.ts
const worker = new Worker("./worker.ts");
worker.postMessage({ keys: ["a", "b", "c", "d"] });
worker.onmessage = (e) => console.log("Processing completed:", e.data);
// worker.ts
declare var self: Worker;
self.onmessage = async (e) => {
const redis = new Bun.RedisClient("redis://localhost:6379");
const results = [];
// exist Worker Run in thread CPU dense logic
for (const key of e.data.keys) {
const value = await redis.get(key);
results.push(transform(value)); // hypothesis transform Very time consuming
}
self.postMessage(results);
};
Performance gap (Bun 1.3.14 vs Node.js 24.6.0)
postMessage Pass 3MB string:
| runtime | Time consuming | memory peak | | :-- | :-- | :-- | | Bun 1.3.14 | 593 ns | Very rarely | | Bun 1.2.21 | 326,290ns | Much | | Node.js 24.6.0 | 242,110 ns | 22 times more |
Key principles: The string in the JavaScriptCore engine isThread-safe reference-counted objects, Bun simplyzero copyPassing pointers is not serialized at all. Node.js also uses the structured cloning algorithm for full replication.
Practical combat: General Worker Pool
type Task = { id: number; key: string };
type Result = { id: number; value: string | null };
export class RedisWorkerPool {
private workers: Worker[] = [];
private queue: Task[] = [];
private busy = new Set<Worker>();
private results: Result[] = [];
private expected = 0;
private onAllDone?: () => void;
constructor(workerPath: string, size = 4) {
for (let i = 0; i < size; i++) {
const w = new Worker(workerPath);
w.onmessage = (e) => this.handleResult(w, e.data);
this.workers.push(w);
}
}
process(tasks: Task[]): Promise<Result[]> {
this.expected = tasks.length;
this.results = [];
this.queue.push(...tasks);
this.dispatch();
return new Promise((resolve) => {
this.onAllDone = () => resolve(this.results);
});
}
private dispatch() {
while (this.queue.length > 0) {
const idle = this.workers.find((w) => !this.busy.has(w));
if (!idle) break;
const task = this.queue.shift()!;
this.busy.add(idle);
idle.postMessage(task);
}
}
private handleResult(worker: Worker, result: Result) {
this.busy.delete(worker);
this.results.push(result);
if (this.results.length >= this.expected) {
// All tasks are completed and the outer layer is triggered resolve
this.onAllDone?.();
} else {
this.dispatch();
}
}
terminate() {
this.workers.forEach((w) => w.terminate());
}
}
illustrate: Previous version of the article
process()insidePromisewill never resolve——handleResultWhen reaching the "all tasks completed" branch, only an empty comment is left. Added hereexpectedcount +onAllDonecallback, letprocess()can actually return results.
Applicable scenarios:
| scene | Do you need a Worker? | | :-- | :-- | | Ordinary GET / SET / HGETALL | ❌ No need, async/await is enough | | Batch Pipeline tens of thousands of keys | ❌ No need, Bun.redis handles it by itself | | After getting the data, do complex calculations (JSON parsing, encryption, aggregation) | ✅ Use Workers to avoid blocking | | Process multiple independent tasks in parallel (such as running 5 reports at the same time) | ✅ One worker per task | | Serialization/deserialization of large amounts of data | ✅ Worker isolation |
9. Comparison table: Why is it worth switching?
| Dimensions | Bun.redis | ioredis | node-redis | | :-- | :-- | :-- | :-- | | Installation volume | 0 KB | ~150 KB | ~200 KB | | Cold start overhead | < 0.1ms | 2-3ms | 3-5ms | | Throughput (pipeline) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | Memory usage | extremely low | in | in | | TypeScript | Native | Required types | Required types | | automatic pipeline | ✅ | ❌ | ❌ | | Cluster complexity | low | in | in | | postMessage(3MB string) | 593ns | — | 242,110 ns | | ecological maturity | New (1.0+) | Veteran (10 years) | Veteran (10 years) | | Documentation / Stack Overflow | less | extremely rich | extremely rich |
Migration costs:
-
• API is 90% similar, common commands (GET/SET/HSET/ZADD/EXPIRE) have the same names
-
• The biggest difference: ioredis’s chaining → Bun.redis’s async/await
-
• Redis Cluster, Pub/Sub, and Stream can be used
10. Practical combat: 3 lines of code to build a Redis cache layer
// cache.ts
const redis = new Bun.RedisClient("redis://localhost:6379");
export async function cache<T>(key: string, ttl: number, fetcher: () => Promise<T>): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const fresh = await fetcher();
// ⚠️ Bun.redis of set() Option objects are not supported, need to go send Set to expire
await redis.send("SET", [key, JSON.stringify(fresh), "EX", String(ttl)]);
return fresh;
}
// use
const user = await cache("user:1", 60, () => db.user.find(1));
No ioredis,No @types/ioredis, no version warning——It’s that simple.
11. Note: Scenarios where Bun.redis cannot be used yet
To be honest,Bun.redis is not for everyone:
| scene | Suggestions | | :-- | :-- | | Need to run in Node.js | ❌ Use ioredis (API is completely different) | | Used ioredis' advanced plug-ins (Sentinel, Cluster complex configuration) | ⚠️ Evaluate migration costs | | There is already a lot of ioredis code | ⚠️ One-time migration is not cost-effective | | Bun version < 1.0 | ❌ The Redis client is not yet stable | | Team is not familiar with Bun | ⚠️ Learning curve |
12. Summary
The real strengths of Bun.redis can be summed up in 6 words:
In short: Bun.redis is not a "Redis client library"; "Built-in infrastructure" for the Bun runtime- like fetch, Bun.file, Bun.serve Likewise, you should use it when writing Bun code.
If you want to "squeeze the last bit of performance out of CPU + Redis", then throw it into a Worker - a 4-core machine, 4x speedup, and that's it.
📖 References
-
• Bun official documentation - Redis Client
-
• Bun Official Documentation - Workers
-
• Bun 1.3.14 postMessage performance optimization
-
• Bun performance benchmark comparison
-
• RESP protocol specification