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+ HELLO command support). Redis 5 and below report ERR 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_uring direct 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() inside Promise will never resolve——handleResult When reaching the "all tasks completed" branch, only an empty comment is left. Added here expected count + onAllDone callback, let process() 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