Bun's Built-in Redis Client: Fast, Simple, Production-Ready
Redis is the backbone of modern applications—powering caching, real-time features, pub/sub messaging, and rate limiting. With Bun 1.3, you now have a first-class, built-in Redis client that's incredibly fast, fully typed, and production-ready out of the box.
No more installing node-redis, ioredis, or configuring connection pools. Just import and go. Let's dive into what makes Bun's Redis client special and how to use it effectively.
Zero-Configuration Setup
Getting started with Bun's Redis client couldn't be simpler. The default client automatically reads from your environment variables:
import { redis } from "bun";
// Reads from REDIS_URL or VALKEY_URL
// Defaults to redis://localhost:6379
await redis.set("greeting", "Hello from Bun!");
const value = await redis.get("greeting"); // "Hello from Bun!"
console.log(value);
That's it. No connection setup, no pool configuration, no callbacks. Just clean, Promise-based API that works immediately.
Environment Variables
Bun looks for these environment variables (in order of priority):
REDIS_URL- Standard Redis connection stringVALKEY_URL- For Valkey (Redis fork) connections- Falls back to
redis://localhost:6379
Example connection strings:
# Standard
REDIS_URL=redis://localhost:6379
# With authentication
REDIS_URL=redis://username:password@localhost:6379
# With database number
REDIS_URL=redis://localhost:6379/0
# TLS connection
REDIS_URL=rediss://localhost:6379
# Unix socket
REDIS_URL=redis+unix:///var/run/redis.sock
Comprehensive Command Support
Bun's Redis client supports 66 commands out of the box, covering the most common use cases:
String Operations
// Basic get/set
await redis.set("user:1:name", "Alice");
const name = await redis.get("user:1:name"); // "Alice"
// Expiration
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // 1 hour TTL
const ttl = await redis.ttl("session:123"); // 3600
// Binary data
const buffer = await redis.getBuffer("user:1:name"); // Buffer<Uint8Array>
Numeric Operations
await redis.set("counter", "0");
// Increment
await redis.incr("counter"); // 1
await redis.incrby("counter", 5); // 6
// Decrement
await redis.decr("counter"); // 5
await redis.decrby("counter", 2); // 3
Hash Operations (HSET, HGET, HMGET, HMSET, HINCRBY, etc.)
// Set multiple fields
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com"]);
// Get specific fields
const userFields = await redis.hmget("user:123", ["name", "email"]);
// ["Alice", "alice@example.com"]
// Get single field
const userName = await redis.hget("user:123", "name"); // "Alice"
// Increment hash field
await redis.hincrby("user:123", "visits", 1); // 1
await redis.hincrbyfloat("user:123", "score", 1.5); // 1.5
Set Operations (SADD, SREM, SMEMBERS, SISMEMBER, etc.)
// Add members
await redis.sadd("tags", "javascript", "typescript", "bun");
// Check membership
const isMember = await redis.sismember("tags", "javascript"); // true
// Get all members
const allTags = await redis.smembers("tags"); // ["javascript", "typescript", "bun"]
// Remove member
await redis.srem("tags", "typescript");
List Operations (LPUSH, LRANGE, etc.)
// Push to list
await redis.lpush("messages", "hello", "world");
// Get range
const messages = await redis.lrange("messages", 0, -1); // ["world", "hello"]
Pub/Sub Messaging
Real-time features like notifications, chat, and live updates rely on Redis pub/sub. Bun's client makes this straightforward:
import { RedisClient } from "bun";
// Create separate clients for publisher and subscriber
const publisher = new RedisClient("redis://localhost:6379");
const subscriber = new RedisClient("redis://localhost:6379");
// Subscribe to a channel
await subscriber.subscribe("notifications", (message, channel) => {
console.log(`[${channel}] Received: ${message}`);
});
// Publish messages
await publisher.publish("notifications", "Hello from Bun!");
await publisher.publish("notifications", "Another message");
The callback receives both the message content and the channel name, making it easy to handle multiple channels:
// Subscribe to multiple channels
await subscriber.subscribe("alerts", handleAlert);
await subscriber.subscribe("updates", handleUpdate);
function handleAlert(message: string, channel: string) {
console.log(`🚨 [${channel}] ${message}`);
}
function handleUpdate(message: string, channel: string) {
console.log(`📢 [${channel}] ${message}`);
}
Performance: Built for Speed
Bun's Redis client is implemented in Zig using the Redis Serialization Protocol (RESP3), giving it significant performance advantages over popular JavaScript clients like ioredis.
Automatic Pipelining
Commands are automatically batched and pipelined, reducing network round-trips:
// These commands are automatically pipelined
await redis.set("key1", "value1");
await redis.set("key2", "value2");
await redis.set("key3", "value3");
const values = await redis.mget(["key1", "key2", "key3"]);
As batch size grows, Bun's performance advantage over ioredis increases significantly. This is particularly beneficial for:
- Bulk data imports
- Cache warming
- Batch updates
- Multi-key operations
Native Zig Implementation
The client is implemented directly in Zig, not JavaScript, which means:
- Zero JavaScript overhead for protocol parsing
- Direct memory access for binary data
- Efficient type conversion between Redis and JavaScript types
Type Conversion
Redis types map cleanly to JavaScript types:
| Redis Type | JavaScript Type |
|---|---|
| Integer | number |
| Bulk String | string |
| Null | null |
| Array | Array |
| Boolean (RESP3) | boolean |
| Map (RESP3) | Object |
| Set (RESP3) | Array |
Production-Ready Reliability
Bun's Redis client includes built-in features for production environments:
Automatic Reconnection
const client = new RedisClient("redis://localhost:6379", {
autoReconnect: true, // Default: true
maxRetries: 10, // Default: 10
});
// Connection drops are handled automatically with exponential backoff
// Starting at 50ms, doubling to max 2000ms
Connection Timeouts
const client = new RedisClient("redis://localhost:6379", {
connectionTimeout: 5000, // Default: 10000ms
idleTimeout: 30000, // Default: 0 (no timeout)
});
Offline Queue
When connection is lost, commands are queued and executed once reconnected:
const client = new RedisClient("redis://localhost:6379", {
enableOfflineQueue: true, // Default: true
});
// These commands will queue if disconnected
// and execute when connection is restored
await redis.set("key", "value");
Auto-Pipelining
Commands are automatically pipelined for improved throughput:
const client = new RedisClient("redis://localhost:6379", {
enableAutoPipelining: true, // Default: true
});
Real-World Use Cases
Caching Layer
async function getUserWithCache(userId: string) {
const cacheKey = `user:${userId}`;
const cachedUser = await redis.get(cacheKey);
if (cachedUser) {
return JSON.parse(cachedUser);
}
// Cache miss - fetch from database
const user = await database.getUser(userId);
// Cache for 1 hour
await redis.set(cacheKey, JSON.stringify(user));
await redis.expire(cacheKey, 3600);
return user;
}
Rate Limiting
async function rateLimit(
ip: string,
limit: number = 100,
windowSecs: number = 3600
): Promise<{ limited: boolean; remaining: number }> {
const key = `ratelimit:${ip}`;
// Increment counter
const count = await redis.incr(key);
// Set expiration on first request
if (count === 1) {
await redis.expire(key, windowSecs);
}
const remaining = Math.max(0, limit - count);
const limited = count > limit;
return { limited, remaining };
}
// Usage
const result = await rateLimit("192.168.1.1", 100, 3600);
if (result.limited) {
throw new Error("Rate limit exceeded");
}
Distributed Lock
async function acquireLock(
key: string,
ttl: number = 30000
): Promise<boolean> {
// Use SET with NX (only set if not exists) and PX (expiration in milliseconds)
const result = await redis.send("SET", [key, "locked", "NX", "PX", ttl]);
return result === "OK";
}
async function releaseLock(key: string): Promise<void> {
await redis.del(key);
}
// Usage
const acquired = await acquireLock("resource:123", 30000);
if (acquired) {
try {
// Perform work
await processResource(123);
} finally {
await releaseLock("resource:123");
}
}
Session Storage
async function createSession(userId: string, data: any) {
const sessionId = crypto.randomUUID();
const sessionKey = `session:${sessionId}`;
await redis.hset(sessionKey, ["userId", userId, "data", JSON.stringify(data)]);
await redis.expire(sessionKey, 86400); // 24 hours
return sessionId;
}
async function getSession(sessionId: string) {
const sessionKey = `session:${sessionId}`;
const session = await redis.hgetall(sessionKey);
if (!session) {
return null;
}
return {
userId: session.userId,
data: JSON.parse(session.data),
};
}
Leaderboard with Sorted Sets
async function updateScore(userId: string, score: number) {
await redis.zadd("leaderboard", score, userId);
}
async function getTopPlayers(limit: number = 10) {
return await redis.zrevrange("leaderboard", 0, limit - 1, "WITHSCORES");
}
async function getUserRank(userId: string) {
const rank = await redis.zrevrank("leaderboard", userId);
return rank !== null ? rank + 1 : null; // 0-indexed to 1-indexed
}
Advanced Features
Raw Commands
For commands not directly exposed, use send():
// Server info
const info = await redis.send("INFO", []);
// List operations
await redis.send("LPUSH", ["mylist", "value1", "value2"]);
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);
// Sorted sets
await redis.send("ZADD", ["leaderboard", "100", "user1", "200", "user2"]);
Custom Client Configuration
import { RedisClient } from "bun";
const client = new RedisClient("redis://localhost:6379", {
connectionTimeout: 5000,
idleTimeout: 30000,
autoReconnect: true,
maxRetries: 10,
enableOfflineQueue: true,
enableAutoPipelining: true,
});
// Explicit connection control
await client.connect();
await client.set("key", "value");
client.close();
TLS Connections
// Using rediss:// scheme
const tlsClient = new RedisClient("rediss://localhost:6379");
// Or with explicit TLS config
const customTlsClient = new RedisClient("redis://localhost:6379", {
tls: {
rejectUnauthorized: false, // For self-signed certs
// ... other TLS options
},
});
Current Limitations
Bun's Redis client is production-ready for most use cases, but has some current limitations:
- Redis Sentinel: Not yet supported
- Redis Cluster: Not yet supported (use single-node Redis)
- Transactions: Must use raw
MULTI/EXECcommands
For transactions:
// Use raw commands for transactions
await redis.send("MULTI", []);
await redis.send("SET", ["key1", "value1"]);
await redis.send("SET", ["key2", "value2"]);
const result = await redis.send("EXEC", []);
Check the official Bun Redis documentation for the latest supported commands and features.
Migration from Other Clients
From ioredis
// Before (ioredis)
import Redis from "ioredis";
const redis = new Redis();
// After (Bun)
import { redis } from "bun";
// API is nearly identical!
From node-redis
// Before (node-redis)
import { createClient } from "redis";
const client = createClient();
await client.connect();
await client.set("key", "value");
// After (Bun)
import { redis } from "bun";
await redis.set("key", "value");
The API is Promise-based and intuitive, making migration straightforward.
Conclusion
Bun's built-in Redis client delivers:
- Zero configuration - Works out of the box with environment variables
- 66 commands - Covers all common use cases
- Production-ready - Auto-reconnect, timeouts, queuing, pipelining
- Blazing fast - Native Zig implementation with automatic pipelining
- Simple API - Clean, Promise-based, fully typed
Whether you're building a cache, implementing rate limiting, or powering real-time features with pub/sub, Bun's Redis client has you covered. Drop it into your next Bun application and experience the speed and simplicity firsthand.
For more details, visit the official Bun Redis documentation.