Redis
Use Bun's native Redis client with a Promise-based API
Bun's native Redis client has a Promise-based API with built-in connection management, fully typed responses, and TLS support.
import { redis } from "bun";
// Set a key
await redis.set("greeting", "Hello from Bun!");
// Get a key
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"
// Increment a counter
await redis.set("counter", 0);
await redis.incr("counter");
// Check if a key exists
const exists = await redis.exists("greeting");
// Delete a key
await redis.del("greeting");Getting Started
To use the Redis client, you first need to create a connection:
import { redis, RedisClient } from "bun";
// Using the default client (reads connection info from environment)
// process.env.REDIS_URL is used by default
await redis.set("hello", "world");
const result = await redis.get("hello");
// Creating a custom client
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");By default, the client reads connection information from the following environment variables (in order of precedence):
REDIS_URLVALKEY_URL- If not set, defaults to
"redis://localhost:6379"
Connection Lifecycle
The Redis client automatically handles connections in the background:
// No connection is made until a command is executed
const client = new RedisClient();
// First command initiates the connection
await client.set("key", "value");
// Connection remains open for subsequent commands
await client.get("key");
// Explicitly close the connection when done
client.close();You can also manually control the connection lifecycle:
const client = new RedisClient();
// Explicitly connect
await client.connect();
// Run commands
await client.set("key", "value");
// Disconnect when done
client.close();Basic Operations
String Operations
// Set a key
await redis.set("user:1:name", "Alice");
// Get a key
const name = await redis.get("user:1:name");
// Get a key as Uint8Array
const buffer = await redis.getBuffer("user:1:name");
// Delete a key
await redis.del("user:1:name");
// Check if a key exists
const exists = await redis.exists("user:1:name");
// Set expiration (in seconds)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // expires in 1 hour
// Get time to live (in seconds)
const ttl = await redis.ttl("session:123");Numeric Operations
// Set initial value
await redis.set("counter", "0");
// Increment by 1
await redis.incr("counter");
// Decrement by 1
await redis.decr("counter");Hash Operations
// Set multiple fields in a hash
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com", "active", "true"]);
// Get multiple fields from a hash
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]
// Get single field from hash (returns value directly, null if missing)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"
// Increment a numeric field in a hash
await redis.hincrby("user:123", "visits", 1);
// Increment a float field in a hash
await redis.hincrbyfloat("user:123", "score", 1.5);Set Operations
// Add member to set
await redis.sadd("tags", "javascript");
// Remove member from set
await redis.srem("tags", "javascript");
// Check if member exists in set
const isMember = await redis.sismember("tags", "javascript");
// Get all members of a set
const allTags = await redis.smembers("tags");
// Get a random member
const randomTag = await redis.srandmember("tags");
// Pop (remove and return) a random member
const poppedTag = await redis.spop("tags");Pub/Sub
Bun provides native bindings for the Redis Pub/Sub protocol, added in Bun 1.2.23.
Redis Pub/Sub is experimental. We expect it to be stable, but we're still looking for feedback and areas for improvement.
Basic Usage
Create a publisher in publisher.ts:
import { RedisClient } from "bun";
const writer = new RedisClient("redis://localhost:6739");
await writer.connect();
writer.publish("general", "Hello everyone!");
writer.close();In another file, create the subscriber in subscriber.ts:
import { RedisClient } from "bun";
const listener = new RedisClient("redis://localhost:6739");
await listener.connect();
await listener.subscribe("general", (message, channel) => {
console.log(`Received: ${message}`);
});In one shell, run your subscriber:
$ bun run subscriber.tsand, in another, run your publisher:
$ bun run publisher.tsSubscribing takes over the RedisClient connection: a client with
subscriptions can only call RedisClient.prototype.subscribe(). To send other
commands to Redis, create a separate connection with .duplicate():
import { RedisClient } from "bun";
const redis = new RedisClient("redis://localhost:6379");
await redis.connect();
const subscriber = await redis.duplicate();
await subscriber.subscribe("foo", () => {});
await redis.set("bar", "baz");Publishing
Publish messages with the publish() method:
await client.publish(channelName, message);Subscriptions
Subscribe to channels with the .subscribe() method:
await client.subscribe(channel, (message, channel) => {});Unsubscribe with the .unsubscribe() method:
await client.unsubscribe(); // Unsubscribe from all channels.
await client.unsubscribe(channel); // Unsubscribe a particular channel.
await client.unsubscribe(channel, listener); // Unsubscribe a particular listener.Advanced Usage
Command Execution and Pipelining
The client automatically pipelines commands, improving performance by sending multiple commands in a batch and processing responses as they arrive.
// Commands are automatically pipelined by default
const [infoResult, listResult] = await Promise.all([redis.get("user:1:name"), redis.get("user:2:email")]);To disable automatic pipelining, set the enableAutoPipelining option to false:
const client = new RedisClient("redis://localhost:6379", {
enableAutoPipelining: false,
});Raw Commands
Use the send method to run any Redis command, including ones without a dedicated method. The first argument is the command name, and the second is an array of string arguments.
// Run any Redis command
const info = await redis.send("INFO", []);
// LPUSH to a list
await redis.send("LPUSH", ["mylist", "value1", "value2"]);
// Get list range
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);Connection Events
You can register handlers for connection events:
const client = new RedisClient();
// Called when successfully connected to Redis server
client.onconnect = () => {
console.log("Connected to Redis server");
};
// Called when disconnected from Redis server
client.onclose = error => {
console.error("Disconnected from Redis server:", error);
};
// Manually connect/disconnect
await client.connect();
client.close();Connection Status and Monitoring
// Check if connected
console.log(client.connected); // boolean indicating connection status
// Check amount of data buffered (in bytes)
console.log(client.bufferedAmount);Type Conversion
The client automatically converts Redis responses to JavaScript values:
- Integer responses are returned as JavaScript numbers
- Bulk strings are returned as JavaScript strings
- Simple strings are returned as JavaScript strings
- Null bulk strings are returned as
null - Array responses are returned as JavaScript arrays
- Error responses throw JavaScript errors with appropriate error codes
- Boolean responses (RESP3) are returned as JavaScript booleans
- Map responses (RESP3) are returned as JavaScript objects
- Set responses (RESP3) are returned as JavaScript arrays
Special handling for specific commands:
EXISTSreturns a boolean instead of a number (1 becomes true, 0 becomes false)SISMEMBERreturns a boolean (1 becomes true, 0 becomes false)
The following commands disable automatic pipelining:
AUTHINFOQUITEXECMULTIWATCHSCRIPTSELECTCLUSTERDISCARDUNWATCHPIPELINESUBSCRIBEPSUBSCRIBEUNSUBSCRIBEUNPSUBSCRIBE
Connection Options
When creating a client, you can pass options to configure the connection:
const client = new RedisClient("redis://localhost:6379", {
// Connection timeout in milliseconds (default: 10000)
connectionTimeout: 5000,
// Idle timeout in milliseconds (default: 0 = no timeout)
idleTimeout: 30000,
// Whether to automatically reconnect on disconnection (default: true)
autoReconnect: true,
// Maximum number of reconnection attempts (default: 20)
maxRetries: 10,
// Whether to queue commands when disconnected (default: true)
enableOfflineQueue: true,
// Whether to automatically pipeline commands (default: true)
enableAutoPipelining: true,
// TLS options (default: false)
tls: true,
// Alternatively, provide custom TLS config:
// tls: {
// rejectUnauthorized: true,
// ca: "path/to/ca.pem",
// cert: "path/to/cert.pem",
// key: "path/to/key.pem",
// }
});Reconnection Behavior
When a connection is lost, the client automatically attempts to reconnect with exponential backoff:
- The client starts with a small delay (50ms) and doubles it with each attempt
- Reconnection delay is capped at 2000ms (2 seconds)
- The client attempts to reconnect up to
maxRetriestimes (default: 20) - Commands executed during disconnection are:
- Queued if
enableOfflineQueueis true (default) - Rejected immediately if
enableOfflineQueueis false
- Queued if
Supported URL Formats
The Redis client supports various URL formats:
// Standard Redis URL
new RedisClient("redis://localhost:6379");
new RedisClient("redis://localhost:6379");
// With authentication
new RedisClient("redis://username:password@localhost:6379");
// With database number
new RedisClient("redis://localhost:6379/0");
// TLS connections
new RedisClient("rediss://localhost:6379");
new RedisClient("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
// Unix socket connections
new RedisClient("redis+unix:///path/to/socket");
new RedisClient("redis+unix:///path/to/socket");
// TLS over Unix socket
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");Error Handling
The Redis client throws typed errors for different scenarios:
try {
await redis.get("non-existent-key");
} catch (error) {
if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
console.error("Connection to Redis server was closed");
} else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
console.error("Authentication failed");
} else {
console.error("Unexpected error:", error);
}
}Common error codes:
ERR_REDIS_CONNECTION_CLOSED- Connection to the server was closedERR_REDIS_AUTHENTICATION_FAILED- Failed to authenticate with the serverERR_REDIS_INVALID_RESPONSE- Received an invalid response from the server
Example Use Cases
Caching
async function getUserWithCache(userId) {
const cacheKey = `user:${userId}`;
// Try to get from cache first
const cachedUser = await redis.get(cacheKey);
if (cachedUser) {
return JSON.parse(cachedUser);
}
// Not in cache, fetch from database
const user = await database.getUser(userId);
// Store in cache for 1 hour
await redis.set(cacheKey, JSON.stringify(user));
await redis.expire(cacheKey, 3600);
return user;
}Rate Limiting
async function rateLimit(ip, limit = 100, windowSecs = 3600) {
const key = `ratelimit:${ip}`;
// Increment counter
const count = await redis.incr(key);
// Set expiry if this is the first request in window
if (count === 1) {
await redis.expire(key, windowSecs);
}
// Check if limit exceeded
return {
limited: count > limit,
remaining: Math.max(0, limit - count),
};
}Session Storage
async function createSession(userId, data) {
const sessionId = crypto.randomUUID();
const key = `session:${sessionId}`;
// Store session with expiration
await redis.hmset(key, ["userId", userId.toString(), "created", Date.now().toString(), "data", JSON.stringify(data)]);
await redis.expire(key, 86400); // 24 hours
return sessionId;
}
async function getSession(sessionId) {
const key = `session:${sessionId}`;
// Get session data
const exists = await redis.exists(key);
if (!exists) return null;
const [userId, created, data] = await redis.hmget(key, ["userId", "created", "data"]);
return {
userId: Number(userId),
created: Number(created),
data: JSON.parse(data),
};
}Implementation Notes
Bun's Redis client is implemented in Rust and uses the Redis Serialization Protocol (RESP3). It reconnects automatically with exponential backoff and pipelines commands, so multiple commands can be sent without waiting for replies to previous ones.
Limitations and Future Plans
Limitations we plan to address in future versions:
- Transactions (MULTI/EXEC) must be done through raw commands
Unsupported features:
- Redis Sentinel
- Redis Cluster