
Redis Js
- 435 installs
- 959 repo stars
- Updated July 31, 2026
- upstash/redis-js
redis-js is an agent skill that guides the @upstash/redis JavaScript SDK for serverless Redis operations for developers who need caching, sessions, rate limiting, and typed data structures without manual serialization.
About
redis-js is an Upstash agent skill bundled in the upstash/redis-js repository with 23 topic guides across five categories: 7 data-structure files (strings, hashes, lists, sets, sorted sets, streams, JSON), 3 advanced-feature files (auto-pipeline, pipelines/transactions, Lua scripting), 5 pattern files (caching, distributed locks, leaderboards, rate limiting, session management), 6 performance files, and 2 migration guides from ioredis and node-redis. The main SKILL.md (175 lines) indexes automatic JavaScript type serialization, common LLM mistakes like manual JSON stringification, full-text search, and auto-pipelining. Developers reach for redis-js when wiring @upstash/redis into Next.js, serverless functions, or agent backends for session caching, API rate limits, leaderboards, or migrating from ioredis.
- redis-js
- AI & Agent Building
- AI-coding skill
Redis Js by the numbers
- 435 all-time installs (skills.sh)
- +11 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,876 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/upstash/redis-js --skill redis-jsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 435 |
|---|---|
| repo stars | ★ 959 |
| Last updated | July 31, 2026 |
| Repository | upstash/redis-js ↗ |
How do you use Upstash Redis in Node.js apps?
Helps with ai & agent building tasks.
Who is it for?
Node.js and TypeScript developers integrating serverless @upstash/redis for caching, sessions, rate limiting, or leaderboards without managing Redis servers.
Skip if: Self-hosted Redis on VMs requiring ioredis cluster features or projects with zero Redis dependency in the stack.
When should I use this skill?
The developer mentions @upstash/redis, Upstash caching, serverless Redis sessions, rate limiting with Redis, or migrating from ioredis to Upstash.
What you get
Redis client setup, typed cache patterns, rate limiters, session stores, migration notes, and pipeline configurations
- Redis client configuration
- Cache/rate-limit patterns
- Migration checklist from ioredis
By the numbers
- Bundles 23 topic skill files across 5 categories
- Covers 7 Redis data-structure guides in skills/data-structures/
- Main SKILL.md is 175 lines indexing all topic guides
Files
Upstash Redis SDK - Complete Skills Guide
This directory contains comprehensive guides for using the @upstash/redis SDK. These skill files are designed to help developers and AI assistants understand and use the SDK effectively.
Installation
npm install @upstash/redisQuick Start
Basic Initialization
import { Redis } from "@upstash/redis";
// Initialize with explicit credentials
const redis = new Redis({
url: "UPSTASH_REDIS_REST_URL",
token: "UPSTASH_REDIS_REST_TOKEN",
});
// Or initialize from environment variables
const redis = Redis.fromEnv();Environment Variables
Set these in your .env file:
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-hereSkill Files Overview
Data Structures (skills/data-structures/)
Redis data types with auto-serialization examples:
- strings.md - GET, SET, INCR, DECR, APPEND with automatic type handling
- hashes.md - HSET, HGET, HMGET with object serialization
- lists.md - LPUSH, RPUSH, LRANGE with array handling
- sets.md - SADD, SMEMBERS, set operations
- sorted-sets.md - ZADD, ZRANGE, ZRANK, leaderboard patterns
- json.md - JSON.SET, JSON.GET, JSONPath queries for nested objects
- streams.md - XADD, XREAD, XGROUP, consumer groups
Advanced Features (skills/advanced-features/)
Complex operations and optimizations:
- auto-pipeline.md - Automatic request batching, performance optimization
- pipeline-and-transactions.md - Manual pipelines, MULTI/EXEC, WATCH for atomic operations
- scripting.md - Lua scripts, EVAL, EVALSHA for server-side logic
Patterns (skills/patterns/)
Common use cases and architectural patterns:
- caching.md - Cache-aside, write-through, TTL strategies
- rate-limiting.md - Integration with @upstash/ratelimit package
- session-management.md - Session storage and user state management
- distributed-locks.md - Lock implementations, deadlock prevention
- leaderboard.md - Sorted set leaderboards, real-time rankings
Performance (skills/performance/)
Optimization techniques and best practices:
- batching-operations.md - MGET, MSET, batch operations
- pipeline-optimization.md - When to use pipelines, performance tips
- ttl-expiration.md - Key expiration strategies, memory management
- data-serialization.md - Deep dive into auto serialization, custom serializers, edge cases
- error-handling.md - Error types, retry strategies, timeout handling, debugging tips
- redis-replicas.md - Global database setup, read replicas, read-your-writes consistency
Search (skills/search/)
Full-text search, filtering, and aggregation extension for Redis:
- overview.md - Schema definition, field types, pitfalls, package overview
- commands/querying.md - Query and count with filters, pagination, sorting, highlighting
- commands/aggregating.md - Metric aggregations ($avg, $sum, $stats), bucket aggregations ($terms, $range, $histogram, $facet)
- commands/index-management.md - Create, describe, drop indexes, waitIndexing
- commands/aliases.md - Index aliases for zero-downtime reindexing
- adapters.md - Using search with node-redis and ioredis via @upstash/search-redis and @upstash/search-ioredis
Migrations (skills/migrations/)
Migration guides from other libraries:
- from-ioredis.md - Migration from ioredis, key differences, serialization changes
- from-redis-node.md - Migration from node-redis, API differences
Common Mistakes (Especially for LLMs)
❌ Mistake 1: Treating Everything as Strings
// ❌ WRONG - Don't do this with @upstash/redis
await redis.set("count", "42"); // Stored as string "42"
const count = await redis.get("count");
const incremented = parseInt(count) + 1; // Manual parsing needed
// ✅ CORRECT - Let the SDK handle it
await redis.set("count", 42); // Stored as number
const count = await redis.get("count");
const incremented = count + 1; // Just use it❌ Mistake 2: Manual JSON Serialization
// ❌ WRONG - Unnecessary with @upstash/redis
await redis.set("user", JSON.stringify({ name: "Alice" }));
const user = JSON.parse(await redis.get("user"));
// ✅ CORRECT - Automatic handling
await redis.set("user", { name: "Alice" });
const user = await redis.get("user");Quick Command Reference
// Strings
await redis.set("key", "value");
await redis.get("key");
await redis.incr("counter");
await redis.decr("counter");
// Hashes
await redis.hset("user:1", { name: "Alice", age: 30 });
await redis.hget("user:1", "name");
await redis.hgetall("user:1");
// Lists
await redis.lpush("tasks", "task1", "task2");
await redis.rpush("tasks", "task3");
await redis.lrange("tasks", 0, -1);
// Sets
await redis.sadd("tags", "javascript", "redis");
await redis.smembers("tags");
// Sorted Sets
await redis.zadd("leaderboard", { score: 100, member: "player1" });
await redis.zrange("leaderboard", 0, -1);
// JSON
await redis.json.set("user:1", "$", { name: "Alice", address: { city: "NYC" } });
await redis.json.get("user:1");
// Expiration
await redis.setex("session", 3600, { userId: "123" });
await redis.expire("key", 60);
await redis.ttl("key");Best Practices
1. Use environment variables for credentials, never hardcode 2. Leverage auto-serialization - pass native JavaScript types 3. Use TypeScript types for better type safety 4. Set appropriate TTLs to manage memory 5. Use pipelines for multiple operations 6. Namespace your keys (e.g., user:123, session:abc)
Resources
Getting Help
For detailed information on specific topics, refer to the individual skill files in the skills/ directory. Each file contains comprehensive examples, use cases, and best practices for its topic.
Automatic Pipelining
Overview
Automatic pipelining batches multiple Redis commands into a single HTTP request, reducing round-trip latency for independent operations.
It's enabled by default. Use enableAutoPipelining: false to disable automatic pipelining.
Good For
- Multiple independent GET/SET operations
- Batch reads across different keys
- Reducing latency in high-latency networks
- Serverless environments with cold starts
Limitations
- Only works with independent commands (no command depends on another's result)
- Not beneficial for single commands
Examples
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Without auto-pipeline: 3 separate HTTP requests
// With auto-pipeline: 1 HTTP request containing all 3 commands
const [user, posts, comments] = await Promise.all([
redis.get("user:1"),
redis.get("posts:1"),
redis.get("comments:1"),
]);
// Auto-pipeline batches these independent operations
async function fetchUserData(userId: string) {
const [profile, settings, activity] = await Promise.all([
redis.hgetall(`user:${userId}:profile`),
redis.hgetall(`user:${userId}:settings`),
redis.zrange(`user:${userId}:activity`, 0, 9),
]);
return { profile, settings, activity };
}Pipelines and Transactions
Overview
Pipelines batch multiple commands for efficiency. Transactions (MULTI/EXEC) execute commands atomically.
Good For
- Pipelines: Reducing round trips for independent operations
- Transactions: Atomic operations that must succeed or fail together
Limitations
- Pipeline commands execute independently (no atomicity)
- Transactions block other clients from modifying watched keys
- WATCH only works for keys, not values
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Manual Pipeline - batch operations for efficiency
const pipeline = redis.pipeline();
pipeline.set("user:1:name", "Alice");
pipeline.set("user:1:email", "alice@example.com");
pipeline.incr("user:count");
pipeline.lpush("recent:users", "user:1");
const results = await pipeline.exec();
// Returns array of results: [OK, OK, 1, 1]
// Transaction (MULTI/EXEC) - atomic operations
const tx = redis.multi();
tx.decrby("inventory:item:1", 5); // Deduct inventory
tx.incrby("user:123:purchases", 5); // Add to user purchases
tx.lpush("orders", JSON.stringify({ userId: 123, itemId: 1, qty: 5 }));
const txResults = await tx.exec();
// All commands succeed together or all failLua Scripting
Overview
Execute Lua scripts atomically on Redis server. Scripts run as a single atomic operation with access to all Redis commands.
Good For
- Complex atomic operations
- Conditional logic on the server
- Reducing round trips for multi-step operations
Limitations
- Blocks other operations while executing
- Scripts should be fast (avoid heavy computation)
- Debugging can be challenging
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Simple script: conditional increment
const script = `
local current = redis.call('GET', KEYS[1])
if current and tonumber(current) < tonumber(ARGV[1]) then
return redis.call('INCR', KEYS[1])
end
return current
`;
const result = await redis.eval(script, ["counter"], [100]);
// Increments only if current value < 100
// Atomic rate limiter
const rateLimitScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
return 0
end
return 1
`;
const allowed = await redis.eval<number>(
rateLimitScript,
["ratelimit:user:123"],
[10, 60] // 10 requests per 60 seconds
);
if (allowed === 1) {
console.log("Request allowed");
} else {
console.log("Rate limit exceeded");
}
// Script with multiple operations
const purchaseScript = `
local inventory = KEYS[1]
local userBalance = KEYS[2]
local qty = tonumber(ARGV[1])
local price = tonumber(ARGV[2])
local stock = tonumber(redis.call('GET', inventory) or 0)
local balance = tonumber(redis.call('GET', userBalance) or 0)
local cost = qty * price
if stock < qty then
return {err = "insufficient_stock"}
end
if balance < cost then
return {err = "insufficient_balance"}
end
redis.call('DECRBY', inventory, qty)
redis.call('DECRBY', userBalance, cost)
return {ok = "success"}
`;
const purchase = await redis.eval<{ err?: string; ok?: string }>(
purchaseScript,
["inventory:item:1", "balance:user:123"],
[5, 20] // Buy 5 items at 20 each
);
// Cache script with EVALSHA for better performance
const scriptSha = await redis.scriptLoad(rateLimitScript);
// Use cached script (faster)
const allowed2 = await redis.evalsha<number>(scriptSha, ["ratelimit:user:456"], [10, 60]);
// Conditional update script
const setIfHigherScript = `
local key = KEYS[1]
local newValue = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or 0)
if newValue > current then
redis.call('SET', key, newValue)
return 1
end
return 0
`;
const updated = await redis.eval<number>(setIfHigherScript, ["high_score:user:123"], [1500]);
console.log(updated === 1 ? "New high score!" : "Score not higher");Hashes
Overview
Hashes store field-value pairs under a single key. They're optimized for storing objects with multiple attributes.
Good For
- User profiles
- Product details
- Configuration settings
- Any structured data with named fields
Limitations
- For nested objects, use JSON data type instead
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
await redis.hset("user:1", {
name: "Alice",
email: "alice@example.com",
age: 30,
});
// Get single field
const name = await redis.hget("user:1", "name"); // "Alice"
// Get multiple fields
const fields = await redis.hmget("user:1", "name", "email"); // ["Alice", "alice@example.com"]
// Get all fields and values
const user = await redis.hgetall("user:1");
// { name: "Alice", email: "alice@example.com", age: 30 }
// Get all field names
const fieldNames = await redis.hkeys("user:1"); // ["name", "email", "age"]
// Get all values
const values = await redis.hvals("user:1"); // ["Alice", "alice@example.com", 30]
// Check if field exists
const hasEmail = await redis.hexists("user:1", "email"); // 1 if exists, 0 if not
// Get number of fields
const fieldCount = await redis.hlen("user:1"); // 3
// Increment numeric field
await redis.hset("user:1", { loginCount: 0 });
await redis.hincrby("user:1", "loginCount", 1); // 1
await redis.hincrby("user:1", "loginCount", 5); // 6
// Increment float field
await redis.hset("user:1", { balance: 100.5 });
await redis.hincrbyfloat("user:1", "balance", 25.75); // 126.25
// Set only if field doesn't exist
await redis.hsetnx("user:1", "verified", "true"); // Returns 1 if set, 0 if exists
// Delete fields
await redis.hdel("user:1", "age");
await redis.hdel("user:1", "loginCount", "balance"); // Delete multiple fields
// Delete entire hash
await redis.del("user:1");JSON
Overview
JSON data type stores JSON documents with support for nested objects and arrays. It enables operations on specific paths within the document using JSONPath syntax.
Good For
- Complex nested objects
- Document storage (user profiles, product catalogs)
- Partial updates of large objects
- Querying nested data
Limitations
- Maximum document size: 512 MB
- JSONPath queries have performance cost on deeply nested structures
- Some JSONPath features may not be supported
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Set entire JSON document
await redis.json.set("user:1", "$", {
name: "Alice",
age: 30,
email: "alice@example.com",
address: {
city: "New York",
country: "USA",
zip: "10001",
},
hobbies: ["reading", "coding"],
metadata: {
registered: "2024-01-15",
verified: true,
},
});
// Get entire document
const user = await redis.json.get("user:1");
// Returns the full object
// Get specific path
const name = await redis.json.get("user:1", "$.name"); // ["Alice"]
const city = await redis.json.get("user:1", "$.address.city"); // ["New York"]
// Get multiple paths
const details = await redis.json.get("user:1", "$.name", "$.email");
// { "$.name": ["Alice"], "$.email": ["alice@example.com"] }
// Set nested field
await redis.json.set("user:1", "$.address.city", "San Francisco");
// Set multiple fields (overwrites at path)
await redis.json.set("user:1", "$.metadata", {
registered: "2024-01-15",
verified: true,
lastLogin: "2024-01-23",
});
// Type-specific operations on JSON
await redis.json.set("product:1", "$", {
name: "Laptop",
price: 999.99,
stock: 50,
tags: ["electronics", "computers"],
});
// Increment numeric field
await redis.json.numincrby("product:1", "$.price", 100); // 1099.99
await redis.json.numincrby("product:1", "$.stock", -5); // 45
// String append
await redis.json.strappend("product:1", "$.name", " Pro"); // "Laptop Pro"
// Get string length
const nameLength = await redis.json.strlen("product:1", "$.name"); // [10]
// Array operations
await redis.json.set("cart:1", "$", {
items: ["item1", "item2"],
quantities: [1, 2],
});
// Append to array
await redis.json.arrappend("cart:1", "$.items", "item3", "item4");
// items: ["item1", "item2", "item3", "item4"]
// Insert into array at index
await redis.json.arrinsert("cart:1", "$.items", 1, "item-new");
// items: ["item1", "item-new", "item2", "item3", "item4"]
// Get array length
const itemCount = await redis.json.arrlen("cart:1", "$.items"); // [5]
// Get element by index
const firstItem = await redis.json.arrindex("cart:1", "$.items", "item1"); // [0]
// Pop from array
const lastItem = await redis.json.arrpop("cart:1", "$.items"); // ["item4"]
const firstFromItems = await redis.json.arrpop("cart:1", "$.items", 0); // ["item1"]
// Trim array to range
await redis.json.arrtrim("cart:1", "$.items", 0, 1); // Keep first 2 elements
// Object operations
await redis.json.set("config:1", "$", {
database: { host: "localhost", port: 5432 },
cache: { enabled: true },
});
// Get object keys
const dbKeys = await redis.json.objkeys("config:1", "$.database");
// [["host", "port"]]
// Get object length (number of keys)
const dbKeyCount = await redis.json.objlen("config:1", "$.database"); // [2]
// Delete path
await redis.json.del("user:1", "$.metadata.lastLogin");
// Delete entire document
await redis.json.del("user:1");
// Type checking
await redis.json.set("data:1", "$", { str: "hello", num: 42, bool: true, arr: [1, 2] });
const type = await redis.json.type("data:1", "$.num"); // ["number"]Lists
Overview
Lists are ordered collections of strings. They support operations at both ends (head and tail) with O(1) time complexity.
Good For
- Activity feeds (latest posts, news)
- Job queues
- Recent items (last 100 searches)
- Stack or queue implementations
Limitations
- Maximum length: 2^32 - 1 (4.2 billion) elements
- Accessing elements by index is O(N) for large lists
- No built-in deduplication (use sets for unique values)
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Push to left (head) of list
await redis.lpush("tasks", "task1");
await redis.lpush("tasks", "task2", "task3"); // ["task3", "task2", "task1"]
// Push to right (tail) of list
await redis.rpush("tasks", "task4"); // ["task3", "task2", "task1", "task4"]
// Get list length
const length = await redis.llen("tasks"); // 4
// Get range of elements (0-based index, inclusive)
const allTasks = await redis.lrange("tasks", 0, -1); // All elements
const firstTwo = await redis.lrange("tasks", 0, 1); // ["task3", "task2"]
// Get element by index
const first = await redis.lindex("tasks", 0); // "task3"
const last = await redis.lindex("tasks", -1); // "task4"
// Set element at index
await redis.lset("tasks", 0, "updated-task");
// Pop from left (head)
const leftItem = await redis.lpop("tasks"); // "updated-task"
// Pop from right (tail)
const rightItem = await redis.rpop("tasks"); // "task4"
// Pop multiple elements from left
await redis.lpush("numbers", 1, 2, 3, 4, 5);
const twoItems = await redis.lpop("numbers", 2); // [5, 4]
// Trim list to specified range (keep only indices 0 to 2)
await redis.ltrim("tasks", 0, 2);
// Remove elements by value
await redis.rpush("items", "a", "b", "c", "b", "d");
await redis.lrem("items", 2, "b"); // Remove first 2 occurrences of "b" from left
// Use negative count to remove from right, 0 to remove all
// Insert before or after a pivot element
await redis.linsert("items", "BEFORE", "c", "x"); // Insert "x" before "c"
await redis.linsert("items", "AFTER", "c", "y"); // Insert "y" after "c"
// Delete entire list
await redis.del("tasks");Sets
Overview
Sets are unordered collections of unique strings. They provide fast membership testing and set operations (union, intersection, difference).
Good For
- Unique item tracking (unique visitors, tags)
- Deduplication
- Membership testing
- Set operations (finding common elements between sets)
Limitations
- Maximum members: 2^32 - 1 (4.2 billion) per set
- Unordered (no guaranteed iteration order)
- Members must be unique (duplicates are automatically ignored)
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Add members to set (duplicates are ignored)
await redis.sadd("tags", "javascript", "redis", "typescript");
await redis.sadd("tags", "javascript"); // Ignored, already exists
// Get all members
const allTags = await redis.smembers("tags"); // ["javascript", "redis", "typescript"]
// Check if member exists
const hasRedis = await redis.sismember("tags", "redis"); // 1 if exists, 0 if not
// Check multiple members at once
const exists = await redis.smismember("tags", ["redis", "python", "typescript"]);
// [1, 0, 1] - redis exists, python doesn't, typescript exists
// Get number of members
const count = await redis.scard("tags"); // 3
// Remove members
await redis.srem("tags", "typescript");
await redis.srem("tags", "javascript", "redis"); // Remove multiple
// Pop random member (removes and returns)
await redis.sadd("items", "a", "b", "c", "d");
const random = await redis.spop("items"); // Removes and returns random member
const twoRandom = await redis.spop("items", 2); // Remove and return 2 random members
// Get random member without removing
const randomItem = await redis.srandmember("items");
const threeRandom = await redis.srandmember("items", 3);
// Set operations with multiple sets
await redis.sadd("set1", "a", "b", "c");
await redis.sadd("set2", "b", "c", "d");
await redis.sadd("set3", "c", "d", "e");
// Intersection (common elements)
const common = await redis.sinter("set1", "set2"); // ["b", "c"]
const commonAll = await redis.sinter("set1", "set2", "set3"); // ["c"]
// Store intersection result in new set
await redis.sinterstore("result", "set1", "set2");
// Union (all unique elements from all sets)
const union = await redis.sunion("set1", "set2"); // ["a", "b", "c", "d"]
// Store union result
await redis.sunionstore("result", "set1", "set2");
// Difference (elements in first set but not in others)
const diff = await redis.sdiff("set1", "set2"); // ["a"]
const diff2 = await redis.sdiff("set2", "set1"); // ["d"]
// Store difference result
await redis.sdiffstore("result", "set1", "set2");
// Move member from one set to another
await redis.smove("set1", "set2", "a"); // Move "a" from set1 to set2
// Delete entire set
await redis.del("tags");Sorted Sets
Overview
Sorted sets store unique members with associated scores. Members are automatically ordered by score, enabling range queries and rankings.
Good For
- Leaderboards and rankings
- Priority queues
- Time-series data (using timestamp as score)
- Trending content (using engagement score)
Limitations
- Maximum members: 2^32 - 1 (4.2 billion) per sorted set
- Scores are 64-bit floating point numbers
- Members must be unique (updating a member replaces its score)
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Add members with scores
await redis.zadd("leaderboard", { score: 100, member: "player1" });
await redis.zadd(
"leaderboard",
{ score: 250, member: "player2" },
{ score: 150, member: "player3" }
);
// Update score (replaces if member exists)
await redis.zadd("leaderboard", { score: 300, member: "player1" });
// Increment score
await redis.zincrby("leaderboard", 50, "player3"); // player3 now has 200
// Get number of members
const count = await redis.zcard("leaderboard"); // 3
// Get score of a member
const score = await redis.zscore("leaderboard", "player1"); // 300
// Get rank (position) of member (0-based, lowest score = rank 0)
const rank = await redis.zrank("leaderboard", "player3"); // 0 (lowest score)
// Get reverse rank (highest score = rank 0)
const revRank = await redis.zrevrank("leaderboard", "player1"); // 0 (highest score)
// Get range by rank (ascending order)
const bottom2 = await redis.zrange("leaderboard", 0, 1);
// ["player3", "player2"] - without scores
// Get range with scores
const bottom2WithScores = await redis.zrange("leaderboard", 0, 1, { withScores: true });
// [{ member: "player3", score: 200 }, { member: "player2", score: 250 }]
// Get range in descending order (highest scores first)
const top2 = await redis.zrange("leaderboard", 0, 1, { rev: true });
// ["player1", "player2"]
const top2WithScores = await redis.zrange("leaderboard", 0, 1, { withScores: true, rev: true });
// [{ member: "player1", score: 300 }, { member: "player2", score: 250 }]
// Get all members
const all = await redis.zrange("leaderboard", 0, -1);
// Get range by score
const midRange = await redis.zrange("leaderboard", 150, 250, { byScore: true });
// ["player3", "player2"]
// Get range by score (descending)
const midRangeDesc = await redis.zrange("leaderboard", 250, 150, { rev: true, byScore: true });
// ["player2", "player3"]
// Count members in score range
const countInRange = await redis.zcount("leaderboard", 100, 250); // 2
// Remove members
await redis.zrem("leaderboard", "player2");
await redis.zrem("leaderboard", "player1", "player3"); // Remove multiple
// Remove by rank range
await redis.zadd(
"scores",
{ score: 1, member: "a" },
{ score: 2, member: "b" },
{ score: 3, member: "c" }
);
await redis.zremrangebyrank("scores", 0, 0); // Remove lowest score (rank 0)
// Remove by score range
await redis.zremrangebyscore("scores", 2, 3); // Remove members with scores 2-3
// Pop members (remove and return)
await redis.zadd("items", { score: 1, member: "a" }, { score: 2, member: "b" });
const lowest = await redis.zpopmin("items"); // Remove and return lowest score
const highest = await redis.zpopmax("items"); // Remove and return highest score
// Pop multiple members
await redis.zadd(
"items",
{ score: 1, member: "a" },
{ score: 2, member: "b" },
{ score: 3, member: "c" }
);
const twoLowest = await redis.zpopmin("items", 2); // Remove 2 lowest
// Practical example: Time-series data
const now = Date.now();
await redis.zadd(
"events",
{ score: now - 3600000, member: "event1" }, // 1 hour ago
{ score: now - 1800000, member: "event2" }, // 30 min ago
{ score: now, member: "event3" } // now
);
// Get events from last hour
const lastHour = await redis.zrange("events", now - 3600000, now, { byScore: true });
// Delete entire sorted set
await redis.del("leaderboard");Streams
Overview
Streams are append-only logs of entries with unique IDs. They support consumer groups for distributed processing and delivery guarantees.
Good For
- Event sourcing
- Activity logs
- Message queues with multiple consumers
- Real-time data feeds
- Chat systems
Limitations
- Entries cannot be modified after insertion
- Memory grows unbounded unless trimmed
- Maximum stream length: practical limits based on memory
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Add entry to stream (auto-generated ID)
const id1 = await redis.xadd("events", "*", {
type: "user.login",
userId: "123",
timestamp: Date.now(),
});
// Returns ID like "1642424242424-0"
// Add with specific ID (timestamp-sequence format)
const id2 = await redis.xadd("events", "1642424242424-1", {
type: "user.logout",
userId: "123",
});
// Add with trim (limit stream size)
await redis.xadd(
"events",
"*",
{ event: "test" },
{
trim: {
type: "MAXLEN",
comparison: "=",
threshold: 1000,
},
}
);
// Add with approximate trim (more efficient)
await redis.xadd(
"events",
"*",
{ event: "test" },
{
trim: {
type: "MAXLEN",
comparison: "~",
threshold: 1000,
},
}
);
// Get stream length
const length = await redis.xlen("events");
// Read entries from stream
const entries = await redis.xread("events", "0", { count: 10 });
// Returns: [{ name: "events", messages: [{ id: "...", message: {...} }] }]
// Read latest entries
const latest = await redis.xread("events", "$"); // $ means new entries only
// Read from multiple streams
const multi = await redis.xread(["stream1", "stream2"], ["0", "0"]);
// Note: Blocking (BLOCK option) is not yet supported in Upstash Redis
// Get range of entries by ID
const range = await redis.xrange("events", "-", "+"); // All entries
const specific = await redis.xrange("events", "1642424242424-0", "1642424242424-1");
const last10 = await redis.xrevrange("events", "+", "-", 10); // Last 10 in reverse
// Create consumer group
await redis.xgroup("events", {
type: "CREATE",
group: "processors",
id: "0",
options: { MKSTREAM: true },
});
// Creates group "processors" starting from beginning
// Read as consumer group member
const groupEntries = await redis.xreadgroup(
"processors", // group name
"consumer1", // consumer name
"events", // stream key
">", // > means undelivered messages
{ count: 5 }
);
// Acknowledge processed messages
if (groupEntries && groupEntries[0]) {
const messages = (groupEntries[0] as any)[1] as Array<{ 0: string; 1: any }>;
if (messages && messages.length > 0) {
const messageIds = messages.map((m) => m[0]);
await redis.xack("events", "processors", messageIds);
}
}
// Get pending messages (delivered but not acknowledged)
const pending = await redis.xpending("events", "processors", "-", "+", 10);
// Returns pending entry details
// Get detailed pending info for specific consumer
const pendingDetails = await redis.xpending(
"events", // stream key
"processors", // group
"-", // start
"_+", // end
10, // count
{ consumer: "consumer1" } // optional: specific consumer
);
// Claim pending messages (take over from another consumer)
const claimed = await redis.xclaim(
"events", // stream key
"processors", // group
"consumer2", // new owner (consumer)
3600000, // min idle time (ms)
"1642424242424-0" // message ID(s) to claim
);
// Delete messages
await redis.xdel("events", [id1, id2]);
// Trim stream to maximum length
await redis.xtrim("events", { strategy: "MAXLEN", threshold: 1000, exactness: "=" });
await redis.xtrim("events", { strategy: "MAXLEN", threshold: 1000, exactness: "~" }); // Approximate (more efficient)
// Get consumer group info
const groups = await redis.xinfo("events", { type: "GROUPS" });
// Returns list of consumer groups
// Get consumers in group
const consumers = await redis.xinfo("events", { type: "CONSUMERS", group: "processors" });
// Returns list of consumers and their stats
// Delete consumer from group
await redis.xgroup("events", {
type: "DELCONSUMER",
group: "processors",
consumer: "consumer1",
});
// Delete consumer group
await redis.xgroup("events", { type: "DESTROY", group: "processors" });
// Practical example: Event log with processing
await redis.xadd("orders", "*", {
orderId: "order-123",
status: "pending",
amount: 99.99,
});
// Create processor group
await redis.xgroup("orders", {
type: "CREATE",
group: "order-processors",
id: "0",
options: { MKSTREAM: true },
});
// Worker reads and processes
const orders = await redis.xreadgroup("order-processors", "worker-1", "orders", ">", {
count: 1,
});
if (orders && orders[0]) {
const messages = (orders[0] as any)[1] as Array<{ 0: string; 1: any }>;
if (messages && messages.length > 0) {
const order = messages[0];
// Process order...
console.log("Processing:", order[1]);
// Acknowledge when done
await redis.xack("orders", "order-processors", order[0]);
}
}Strings
Overview
Strings are the most basic Redis data structure. They store a single value and support atomic operations like increment/decrement.
Good For
- Caching simple values (strings, numbers, booleans, objects)
- Counters (page views, likes, inventory)
- Feature flags
- Session tokens
Limitations
- Maximum value size: 512 MB
- No built-in list or set operations on a single key
- Increment/decrement only work on values that can be parsed as integers
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Set and get strings
await redis.set("greeting", "Hello World");
const greeting = await redis.get("greeting"); // "Hello World"
// Set and get numbers (automatic serialization)
await redis.set("views", 100);
const views = await redis.get("views"); // 100 (as number, not string)
// Set and get objects (automatic JSON serialization)
await redis.set("user", { name: "Alice", age: 30 });
const user = await redis.get("user"); // { name: "Alice", age: 30 }
// Set with expiration (TTL in seconds)
await redis.setex("session", 3600, { userId: "123" });
// Set only if key doesn't exist
await redis.setnx("lock", "process-1"); // Returns 1 if set, 0 if already exists
// Increment and decrement counters
await redis.incr("views"); // 101
await redis.decr("views"); // 100
await redis.incrby("views", 10); // 110
await redis.decrby("views", 5); // 105
// Append to string
await redis.set("name", "Alice");
await redis.append("name", " Smith"); // "Alice Smith"
// Get and set atomically
const oldValue = await redis.getset("counter", 0);
// Multiple get/set (batch operations)
await redis.mset({ key1: "value1", key2: "value2", key3: 123 });
const values = await redis.mget("key1", "key2", "key3"); // ["value1", "value2", 123]
// Check if key exists
const exists = await redis.exists("greeting"); // 1 if exists, 0 if not
// Delete key
await redis.del("greeting");
// Get TTL (time to live in seconds)
await redis.ttl("session"); // seconds remaining, -1 if no expiry, -2 if doesn't exist
// Set expiration on existing key
await redis.expire("key1", 60); // expires in 60 secondsMigrating from ioredis
Overview
Migrate from ioredis to @upstash/redis by removing manual serialization, using REST API, and leveraging automatic type preservation.
Good For
- Serverless environments (no TCP connections)
- Reducing boilerplate (no JSON.stringify/parse)
- Type safety with automatic serialization
- Simplified code maintenance
Examples
Basic Comparison
ioredis:
import Redis from "ioredis";
const redis = new Redis({
host: "localhost",
port: 6379,
});
// Manual serialization required
const user = { name: "Alice", age: 30 };
await redis.set("user:1", JSON.stringify(user));
const raw = await redis.get("user:1");
const retrieved = JSON.parse(raw!); // Manual parsing
// Numbers returned as strings
await redis.set("count", "42");
const count = await redis.get("count"); // "42" (string)
const numCount = parseInt(count!, 10); // Convert manually@upstash/redis:
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Automatic serialization
const user = { name: "Alice", age: 30 };
await redis.set("user:1", user); // No JSON.stringify
const retrieved = await redis.get("user:1"); // Automatic parsing
console.log(retrieved.name); // "Alice"
// Numbers preserved
await redis.set("count", 42);
const count = await redis.get("count"); // 42 (number)Connection Initialization
ioredis:
import Redis from "ioredis";
const redis = new Redis({
host: "redis.upstash.io",
port: 6379,
password: "your-password",
});@upstash/redis:
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Or use environment variables
const redis = Redis.fromEnv();Command Syntax
ioredis:
// SET with expiration (positional args)
await redis.set("key", "value", "EX", 3600);
// ZADD
await redis.zadd("leaderboard", 100, "player:1");@upstash/redis:
// SET with expiration (options object)
await redis.set("key", "value", { ex: 3600 });
// ZADD
await redis.zadd("leaderboard", { score: 100, member: "player:1" });Pipelines
ioredis:
const pipeline = redis.pipeline();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.get("key1");
const results = await pipeline.exec();
// Results: [[null, "OK"], [null, "OK"], [null, "value1"]]@upstash/redis:
const pipeline = redis.pipeline();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.get("key1");
const results = await pipeline.exec();
// Results: ["OK", "OK", "value1"]Hash Operations
ioredis:
const hash = { name: "Alice", age: "30" }; // Must be strings
await redis.hmset("user:1", hash);
const retrieved = await redis.hgetall("user:1");
console.log(typeof retrieved.age); // "string"@upstash/redis:
const hash = { name: "Alice", age: 30 }; // Native types
await redis.hset("user:1", hash);
const retrieved = await redis.hgetall("user:1");
console.log(typeof retrieved.age); // "number"Migration Checklist
1. Replace imports: import Redis from "ioredis" → import { Redis } from "@upstash/redis" 2. Update connection: Use REST URL and token instead of host/port 3. Remove JSON.stringify/JSON.parse calls 4. Update command syntax: Positional args → options objects where applicable 5. Remove parseInt/parseFloat for numbers 6. Update ZADD syntax: redis.zadd(key, score, member) → redis.zadd(key, { score, member })
Migrating from node-redis
Overview
Migrate from node-redis (redis@4.x) to @upstash/redis for automatic serialization, REST API access, and serverless-friendly architecture.
Good For
- Serverless deployments
- Eliminating manual type conversions
- Cleaner code without Buffer handling
- REST-based access (no TCP connections)
Limitations
- Different connection model (REST vs TCP)
- Command syntax differences
Examples
Basic Comparison
node-redis:
import { createClient } from "redis";
const redis = createClient({
url: "redis://localhost:6379",
});
await redis.connect();
// Manual serialization
const user = { name: "Alice", age: 30 };
await redis.set("user:1", JSON.stringify(user));
const raw = await redis.get("user:1");
const retrieved = JSON.parse(raw!);
// Numbers as strings
await redis.set("count", "42");
const count = await redis.get("count"); // "42"
const num = parseInt(count!, 10);
await redis.disconnect();@upstash/redis:
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// No connect() needed
// Automatic serialization
const user = { name: "Alice", age: 30 };
await redis.set("user:1", user);
const retrieved = await redis.get("user:1");
// retrieved is already parsed
// Numbers preserved
await redis.set("count", 42);
const count = await redis.get("count"); // 42 (number)
// No disconnect() neededConnection Initialization
node-redis:
import { createClient } from "redis";
const redis = createClient({
socket: {
host: "redis.upstash.io",
port: 6379,
},
password: "your-password",
});
await redis.connect();@upstash/redis:
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});Command Syntax
node-redis:
// SET with expiration
await redis.set("key", "value", { EX: 3600 });
// ZADD
await redis.zAdd("leaderboard", { score: 100, value: "player:1" });
// ZRANGE with scores
const results = await redis.zRangeWithScores("leaderboard", 0, 9);
// Returns: [{ value: "player:1", score: 100 }, ...]@upstash/redis:
// SET with expiration (lowercase options)
await redis.set("key", "value", { ex: 3600 });
// ZADD (member instead of value)
await redis.zadd("leaderboard", { score: 100, member: "player:1" });
// ZRANGE with scores
const results = await redis.zrange("leaderboard", 0, 9, { withScores: true });
// Returns: [{ member: "player:1", score: 100 }, ...]Hash Operations
node-redis:
await redis.hSet("user:1", "name", "Alice");
await redis.hSet("user:1", "age", "30"); // Must be string
const age = await redis.hGet("user:1", "age");
const numAge = parseInt(age!, 10);@upstash/redis:
await redis.hset("user:1", { name: "Alice", age: 30 }); // Native types
const age = await redis.hget("user:1", "age"); // Returns 30 (number)Pipelines
node-redis:
const pipeline = redis.multi();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.get("key1");
const results = await pipeline.exec();@upstash/redis:
const pipeline = redis.pipeline();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.get("key1");
const results = await pipeline.exec();Error Handling
node-redis:
try {
await redis.get("key");
} catch (error) {
console.error(error);
} finally {
await redis.disconnect();
}@upstash/redis:
try {
await redis.get("key");
} catch (error) {
console.error(error);
}
// No cleanup neededMigration Checklist
1. Replace imports: import { createClient } from "redis" → import { Redis } from "@upstash/redis" 2. Remove connection management: await redis.connect() and await redis.disconnect() 3. Update connection config: Use REST URL and token 4. Remove JSON.stringify/JSON.parse calls 5. Update command names: zAdd → zadd, hSet → hset (lowercase) 6. Update options: EX → ex, value → member in sorted sets 7. Remove type conversions: No parseInt, parseFloat needed
Caching Strategies
Overview
Use Redis as a cache layer to reduce database load and improve response times. Supports cache-aside, write-through, and TTL-based expiration.
Good For
- Reducing database queries
- Storing frequently accessed data
- Session data, API responses, computed results
- Temporary data with automatic expiration
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Cache-Aside (Lazy Loading) - most common pattern
async function getUser(userId: string) {
// Try cache first
const cached = await redis.get(`user:${userId}`);
if (cached) {
return cached; // Cache hit
}
// Cache miss - fetch from database
const user = await database.users.findById(userId);
// Store in cache with 1 hour TTL
await redis.set(`user:${userId}`, user, { ex: 3600 });
return user;
}
// Write-Through - update cache on write
async function updateUser(userId: string, data: any) {
// Update database
const user = await database.users.update(userId, data);
// Update cache immediately
await redis.set(`user:${userId}`, user, { ex: 3600 });
return user;
}
// Cache invalidation
async function deleteUser(userId: string) {
// Delete from database
await database.users.delete(userId);
// Invalidate cache
await redis.del(`user:${userId}`);
}Distributed Locks
Overview
Distributed locks prevent concurrent access to shared resources across multiple processes or servers. Use SET NX with expiration for simple locking.
Good For
- Preventing duplicate job execution
- Ensuring only one process modifies a resource
- Rate limiting at system level
- Coordinating distributed operations
Limitations
- Lock holder must complete before TTL expires
- No automatic lock release on crash (relies on TTL)
- Use @upstash/lock for production (implements Redlock)
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Simple lock with SET NX
async function acquireLock(lockKey: string, ttl: number = 10): Promise<boolean> {
const acquired = await redis.set(lockKey, "locked", {
nx: true, // Only set if not exists
ex: ttl, // Expire after ttl seconds
});
return acquired === "OK";
}
async function releaseLock(lockKey: string) {
await redis.del(lockKey);
}
// Use lock pattern
async function processJob(jobId: string) {
const lockKey = `lock:job:${jobId}`;
const acquired = await acquireLock(lockKey, 30);
if (!acquired) {
console.log("Job already being processed");
return;
}
try {
// Do work
await performJobWork(jobId);
} finally {
await releaseLock(lockKey);
}
}
// Lock with unique token (prevents accidental unlock by others)
async function acquireLockWithToken(lockKey: string, token: string, ttl: number = 10) {
const acquired = await redis.set(lockKey, token, { nx: true, ex: ttl });
return acquired === "OK";
}
async function releaseLockWithToken(lockKey: string, token: string) {
// Only delete if token matches (using Lua script)
const script = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`;
return await redis.eval<number>(script, [lockKey], [token]);
}
// Usage with token
async function processWithTokenLock(jobId: string) {
const lockKey = `lock:job:${jobId}`;
const token = crypto.randomUUID();
const acquired = await acquireLockWithToken(lockKey, token, 30);
if (!acquired) return;
try {
await performJobWork(jobId);
} finally {
await releaseLockWithToken(lockKey, token);
}
}
// Lock with retry
async function acquireLockWithRetry(
lockKey: string,
ttl: number = 10,
retries: number = 3,
delay: number = 100
): Promise<boolean> {
for (let i = 0; i < retries; i++) {
const acquired = await acquireLock(lockKey, ttl);
if (acquired) return true;
await new Promise((resolve) => setTimeout(resolve, delay));
}
return false;
}
// Prevent duplicate webhook processing
async function processWebhook(webhookId: string, data: any) {
const lockKey = `webhook:${webhookId}`;
const acquired = await acquireLock(lockKey, 60);
if (!acquired) {
console.log("Webhook already processed");
return { status: "duplicate" };
}
try {
await handleWebhook(data);
return { status: "processed" };
} finally {
await releaseLock(lockKey);
}
}Leaderboard Pattern
Overview
Use Sorted Sets (ZSET) to implement leaderboards with automatic ranking. Scores determine rank, members are unique.
Good For
- Gaming leaderboards (high scores)
- User rankings by activity, points, or reputation
- Top performers, trending content
- Time-based rankings (using timestamps as scores)
Limitations
- Ties have undefined order (use score decimals or timestamps to break ties)
- Memory grows with number of members
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Add/update scores
await redis.zadd("leaderboard:global", { score: 1500, member: "player:123" });
await redis.zadd("leaderboard:global", { score: 2300, member: "player:456" });
await redis.zadd("leaderboard:global", { score: 1800, member: "player:789" });
// Increment score
await redis.zincrby("leaderboard:global", 100, "player:123"); // +100 points
// Get top 10 (highest scores first)
const top10 = await redis.zrange("leaderboard:global", 0, 9, { rev: true, withScores: true });
// Returns: [{ member: "player:456", score: 2300 }, { member: "player:789", score: 1800 }, ...]
// Get player's rank (0-based, lowest score = rank 0)
const rank = await redis.zrevrank("leaderboard:global", "player:123");
// Use zrevrank for highest-first ranking
// Get player's score
const score = await redis.zscore("leaderboard:global", "player:123");
// Get rank with score
async function getPlayerStats(playerId: string) {
const [rank, score, totalPlayers] = await Promise.all([
redis.zrevrank("leaderboard:global", playerId),
redis.zscore("leaderboard:global", playerId),
redis.zcard("leaderboard:global"),
]);
return {
playerId,
score,
rank: rank !== null ? rank + 1 : null, // Convert to 1-based
totalPlayers,
};
}
// Get surrounding players (context ranking)
async function getRankContext(playerId: string, range: number = 5) {
const rank = await redis.zrevrank("leaderboard:global", playerId);
if (rank === null) return null;
const start = Math.max(0, rank - range);
const end = rank + range;
const players = await redis.zrange("leaderboard:global", start, end, {
rev: true,
withScores: true,
});
return players;
}
// Time-based leaderboard (daily)
const today = new Date().toISOString().split("T")[0];
const dailyKey = `leaderboard:daily:${today}`;
await redis.zadd(dailyKey, { score: 500, member: "player:123" });
await redis.expire(dailyKey, 86400 * 7); // Keep for 7 days
// Multiple leaderboards (by region)
await redis.zadd("leaderboard:us", { score: 1500, member: "player:123" });
await redis.zadd("leaderboard:eu", { score: 1500, member: "player:456" });
// Get top from multiple boards
const [usTop, euTop] = await Promise.all([
redis.zrange("leaderboard:us", 0, 9, { rev: true, withScores: true }),
redis.zrange("leaderboard:eu", 0, 9, { rev: true, withScores: true }),
]);
// Score range query (players with score 1000-2000)
const midRange = await redis.zrangebyscore("leaderboard:global", 1000, 2000, {
withScores: true,
});
// Remove player
await redis.zrem("leaderboard:global", "player:123");
// Remove bottom 10% (cleanup low performers)
const total = await redis.zcard("leaderboard:global");
const cutoff = Math.floor(total * 0.1);
await redis.zpopmin("leaderboard:global", cutoff);
// Tie breaking: use timestamp as decimal
const now = Date.now();
const scoreWithTieBreaker = 1500 + now / 1e13; // 1500.000123456
await redis.zadd("leaderboard:global", {
score: scoreWithTieBreaker,
member: "player:999",
});
// Batch update scores
const pipeline = redis.pipeline();
pipeline.zadd("leaderboard:global", { score: 1600, member: "player:1" });
pipeline.zadd("leaderboard:global", { score: 1700, member: "player:2" });
pipeline.zadd("leaderboard:global", { score: 1800, member: "player:3" });
await pipeline.exec();
// Get percentile rank
async function getPercentile(playerId: string) {
const [rank, total] = await Promise.all([
redis.zrevrank("leaderboard:global", playerId),
redis.zcard("leaderboard:global"),
]);
if (rank === null) return null;
return ((rank / total) * 100).toFixed(2);
}Rate Limiting
Overview
Control request rates to prevent abuse and ensure fair resource usage. Use counters with TTL for simple rate limiting, or @upstash/ratelimit for production.
Good For
- API rate limiting (requests per user/IP)
- Preventing brute force attacks
- Throttling expensive operations
- Fair resource allocation
Limitations
- Simple counters can be imprecise at window boundaries
- Distributed rate limiting requires careful coordination
- Use @upstash/ratelimit for production (supports multiple algorithms)
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Simple fixed-window rate limiter
async function simpleRateLimit(userId: string, limit: number = 10, window: number = 60) {
const key = `ratelimit:${userId}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, window);
}
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
reset: window,
};
}
// Usage
const result = await simpleRateLimit("user:123", 10, 60);
if (!result.allowed) {
throw new Error("Rate limit exceeded");
}
// Sliding window rate limiter using sorted set
async function slidingWindowRateLimit(userId: string, limit: number = 10, window: number = 60) {
const key = `ratelimit:sliding:${userId}`;
const now = Date.now();
const windowStart = now - window * 1000;
// Remove old entries
await redis.zremrangebyscore(key, 0, windowStart);
// Count requests in window
const count = await redis.zcard(key);
if (count >= limit) {
return { allowed: false, remaining: 0 };
}
// Add current request
await redis.zadd(key, { score: now, member: `${now}:${Math.random()}` });
await redis.expire(key, window * 2); // Cleanup
return {
allowed: true,
remaining: limit - count - 1,
};
}
// Token bucket using Lua script
const tokenBucketScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1] or capacity)
local last_refill = tonumber(bucket[2] or now)
-- Refill tokens based on time elapsed
local elapsed = now - last_refill
local refill = math.floor(elapsed * rate)
tokens = math.min(capacity, tokens + refill)
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return 1
else
return 0
end
`;
async function tokenBucketRateLimit(userId: string, capacity: number = 10, rate: number = 1) {
const key = `ratelimit:bucket:${userId}`;
const now = Date.now() / 1000;
const allowed = await redis.eval<number>(tokenBucketScript, [key], [capacity, rate, now]);
return { allowed: allowed === 1 };
}
// Production: Use @upstash/ratelimit
// npm install @upstash/ratelimit
import { Ratelimit } from "@upstash/ratelimit";
// Fixed window
const fixedWindowLimiter = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(10, "60 s"),
});
// Sliding window
const slidingWindowLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "60 s"),
});
// Token bucket
const tokenBucketLimiter = new Ratelimit({
redis,
limiter: Ratelimit.tokenBucket(10, "1 s", 10),
});
// Usage
async function handleRequest(userId: string) {
const { success, limit, remaining, reset } = await fixedWindowLimiter.limit(userId);
if (!success) {
return {
error: "Rate limit exceeded",
limit,
remaining,
reset,
};
}
// Process request
return { data: "Success", remaining };
}
// Multi-tier rate limiting
async function multiTierRateLimit(userId: string, tier: "free" | "pro") {
const limits = {
free: { requests: 100, window: 3600 },
pro: { requests: 1000, window: 3600 },
};
const config = limits[tier];
return await simpleRateLimit(userId, config.requests, config.window);
}
// Per-endpoint rate limiting
async function endpointRateLimit(userId: string, endpoint: string) {
const key = `${userId}:${endpoint}`;
return await simpleRateLimit(key, 10, 60);
}
// IP-based rate limiting
async function ipRateLimit(ip: string) {
return await simpleRateLimit(`ip:${ip}`, 100, 60);
}
// Combined rate limiting (both user and IP)
async function combinedRateLimit(userId: string, ip: string) {
const [userLimit, ipLimit] = await Promise.all([
simpleRateLimit(`user:${userId}`, 100, 60),
simpleRateLimit(`ip:${ip}`, 1000, 60),
]);
return {
allowed: userLimit.allowed && ipLimit.allowed,
limits: { user: userLimit, ip: ipLimit },
};
}Recommendation: For production, use @upstash/ratelimit which provides battle-tested algorithms, analytics, and better accuracy.
Session Management
Overview
Store user sessions in Redis with automatic serialization and TTL-based expiration. Sessions contain user state, authentication data, and preferences.
Good For
- User authentication sessions
- Shopping carts
- Temporary user state
- Multi-page form data
- "Remember me" tokens
Limitations
- Sessions expire after TTL (must be renewed)
- Large session objects consume memory
- Shared sessions require coordination across devices
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Create session with 1-hour expiration
async function createSession(userId: string, userData: any) {
const sessionId = crypto.randomUUID();
await redis.set(
`session:${sessionId}`,
{
userId,
...userData,
createdAt: Date.now(),
},
{ ex: 3600 } // 1 hour TTL
);
return sessionId;
}
// Get session
async function getSession(sessionId: string) {
const session = await redis.get<any>(`session:${sessionId}`);
return session;
}
// Update session and refresh TTL
async function updateSession(sessionId: string, updates: any) {
const current = await redis.get<any>(`session:${sessionId}`);
if (!current) {
throw new Error("Session not found");
}
await redis.set(
`session:${sessionId}`,
{ ...current, ...updates, updatedAt: Date.now() },
{ ex: 3600 } // Reset TTL
);
}
// Extend session (refresh TTL without updating data)
async function extendSession(sessionId: string) {
await redis.expire(`session:${sessionId}`, 3600);
}
// Delete session (logout)
async function deleteSession(sessionId: string) {
await redis.del(`session:${sessionId}`);
}
// User login flow
async function login(email: string, password: string) {
// Verify credentials (not shown)
const user = await verifyCredentials(email, password);
if (!user) {
throw new Error("Invalid credentials");
}
// Create session
const sessionId = await createSession(user.id, {
email: user.email,
name: user.name,
role: user.role,
});
return sessionId;
}
// User logout
async function logout(sessionId: string) {
await deleteSession(sessionId);
}
// Session with sliding expiration (extends on each access)
async function getSessionWithSliding(sessionId: string) {
const session = await redis.get<any>(`session:${sessionId}`);
if (session) {
// Extend session on access
await redis.expire(`session:${sessionId}`, 3600);
}
return session;
}
// Shopping cart session
async function addToCartSession(sessionId: string, item: any) {
const key = `cart:${sessionId}`;
const cart = (await redis.get<any[]>(key)) || [];
cart.push(item);
await redis.set(key, cart, { ex: 86400 }); // 24 hour cart
return cart;
}
// Multi-device sessions (track all user sessions)
async function createMultiDeviceSession(userId: string, deviceInfo: any) {
const sessionId = crypto.randomUUID();
// Store session
await redis.set(
`session:${sessionId}`,
{ userId, ...deviceInfo, createdAt: Date.now() },
{ ex: 3600 }
);
// Track in user's session list
await redis.sadd(`user:${userId}:sessions`, sessionId);
return sessionId;
}
// Logout from all devices
async function logoutAllDevices(userId: string) {
const sessions = await redis.smembers(`user:${userId}:sessions`);
// Delete all sessions
const pipeline = redis.pipeline();
sessions.forEach((sessionId) => {
pipeline.del(`session:${sessionId}`);
});
pipeline.del(`user:${userId}:sessions`);
await pipeline.exec();
}
// "Remember me" token (long-lived)
async function createRememberMeToken(userId: string) {
const token = crypto.randomUUID();
await redis.set(
`remember:${token}`,
{ userId, createdAt: Date.now() },
{ ex: 2592000 } // 30 days
);
return token;
}
async function loginWithRememberMe(token: string) {
const data = await redis.get<any>(`remember:${token}`);
if (!data) {
return null;
}
// Create new session
const sessionId = await createSession(data.userId, {});
return sessionId;
}
// Session with user activity tracking
async function trackActivity(sessionId: string, action: string) {
const session = await redis.get<any>(`session:${sessionId}`);
if (!session) return;
session.lastActivity = {
action,
timestamp: Date.now(),
};
await redis.set(`session:${sessionId}`, session, { ex: 3600 });
}
// Clean up expired sessions manually (if needed)
async function cleanupExpiredSessions(userId: string) {
const sessions = await redis.smembers(`user:${userId}:sessions`);
const pipeline = redis.pipeline();
sessions.forEach((sessionId) => {
pipeline.exists(`session:${sessionId}`);
});
const results = await pipeline.exec();
// Remove invalid sessions from set
const invalid = sessions.filter((_, i) => results[i] === 0);
if (invalid.length > 0) {
await redis.srem(`user:${userId}:sessions`, ...invalid);
}
}
// Helper placeholder
async function verifyCredentials(email: string, password: string) {
return { id: "123", email, name: "User", role: "user" };
}Batching Operations
Overview
Batch multiple Redis operations into single commands to reduce network round trips. Use MGET/MSET for strings, HMGET/HMSET for hashes, pipelines for mixed operations.
Good For
- Fetching multiple keys at once
- Bulk data loading
- Reducing latency in high-latency networks
- Operations on related data
Limitations
- Batch operations are atomic per command, not across commands
- Some commands don't have batch equivalents
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// MGET - batch get multiple keys
const [user1, user2, user3] = await redis.mget<any[]>("user:1", "user:2", "user:3");
// MSET - batch set multiple keys
await redis.mset({
"user:1": { name: "Alice", age: 30 },
"user:2": { name: "Bob", age: 25 },
"user:3": { name: "Charlie", age: 35 },
});
// Compare: individual operations (3 round trips)
const u1 = await redis.get("user:1");
const u2 = await redis.get("user:2");
const u3 = await redis.get("user:3");
// vs batch operation (1 round trip)
const users = await redis.mget("user:1", "user:2", "user:3");
// HMGET - batch get hash fields
const [name, email] = await redis.hmget("user:123", "name", "email");
// HMSET - batch set hash fields
await redis.hset("user:123", {
name: "Alice",
email: "alice@example.com",
age: 30,
});
// Batch with mixed operations - use pipeline
const pipeline = redis.pipeline();
userIds.forEach((id) => {
pipeline.get(`user:${id}`);
pipeline.hgetall(`user:${id}:profile`);
pipeline.zrank("leaderboard", id);
});
const results = await pipeline.exec();
// Batch delete
await redis.del("key1", "key2", "key3", "key4");
// Batch SADD
await redis.sadd("tags", "redis", "nodejs", "typescript", "upstash");
// Batch with Promise.all (for independent operations)
const [userCount, postCount, commentCount] = await Promise.all([
redis.get("count:users"),
redis.get("count:posts"),
redis.get("count:comments"),
]);
// Note: With auto-pipelining enabled, these batch automaticallyData Serialization and Deserialization
Overview
Automatic serialization preserves JavaScript types across Redis operations. Numbers stay numbers, objects stay objects, arrays stay arrays.
Good For
- Storing any JavaScript value without JSON.stringify/parse
- Type preservation across GET/SET
- Cleaner code (no manual serialization)
- Storing numbers, booleans, objects, arrays, null
Limitations
- undefined, functions, symbols cannot be serialized
- Date objects serialize as ISO strings
- Class instances lose their prototype
- Binary data requires special handling
Examples
Basic Type Preservation
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Numbers preserved
await redis.set("age", 42);
const age = await redis.get("age");
console.log(typeof age); // "number"
console.log(age === 42); // true
// Compare with other SDKs
// ioredis: returns "42" (string)
// @upstash/redis: returns 42 (number)
// Booleans preserved
await redis.set("active", true);
const active = await redis.get("active");
console.log(typeof active); // "boolean"
// Objects preserved
await redis.set("user", { name: "Alice", age: 30 });
const user = await redis.get("user");
console.log(user.name); // "Alice"
console.log(user.age); // 30 (number, not string)
// Arrays preserved
await redis.set("scores", [100, 200, 300]);
const scores = await redis.get<number[]>("scores");
console.log(scores[0]); // 100 (number)
// Nested structures preserved
await redis.set("data", {
user: { name: "Alice", age: 30 },
scores: [100, 200, 300],
active: true,
count: 42,
});
const data = await redis.get<any>("data");
console.log(typeof age); // "number"
console.log(Array.isArray(data.scores)); // trueComplex Types
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Date objects become ISO strings
await redis.set("created", new Date());
const created = await redis.get<string>("created");
console.log(typeof created); // "string"
console.log(created); // "2024-01-01T00:00:00.000Z"
// Convert back to Date
const createdDate = new Date(created);
// Class instances lose prototype
class User {
constructor(public name: string) {}
greet() {
return `Hello, ${this.name}`;
}
}
const alice = new User("Alice");
await redis.set("instance", alice);
const retrieved = await redis.get<any>("instance");
console.log(retrieved.name); // "Alice"
console.log(retrieved.greet); // undefined (method lost)
// Solution: serialize/deserialize manually
class SerializableUser {
constructor(public name: string) {}
static toRedis(user: SerializableUser) {
return { name: user.name };
}
static fromRedis(data: any) {
return new SerializableUser(data.name);
}
}
await redis.set("ser_user", SerializableUser.toRedis(alice));
const serRetrieved = SerializableUser.fromRedis(await redis.get("ser_user"));Disabling Auto-Serialization
import { Redis } from "@upstash/redis";
// Disable auto-serialization if needed
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
automaticDeserialization: false,
});
await redis.set("key", JSON.stringify({ value: 42 }));
const raw = await redis.get("key");
console.log(typeof raw); // "string"
const parsed = JSON.parse(raw as string);Error Handling
Overview
Handle Redis errors gracefully with try-catch, implement retry logic for transient failures, and provide fallbacks for degraded operation.
Good For
- Network failure recovery
- Timeout handling
- Graceful degradation
- Debugging and monitoring
- Production reliability
Limitations
- Some errors are not recoverable
- Retries can increase latency
- Too many retries may cause cascading failures
Examples
Built-in Retry Configuration
import { Redis } from "@upstash/redis";
// Default: 5 retries with exponential backoff
const redis = Redis.fromEnv();
// Customize retry behavior
const redisWithRetry = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
retry: {
retries: 3,
backoff: (retryCount) => Math.exp(retryCount) * 50, // Exponential backoff
},
});
// Disable retries
const redisNoRetry = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
retry: false, // No retries
});
// Custom backoff strategy
const redisCustomBackoff = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
retry: {
retries: 10,
backoff: (retryCount) => {
// Linear backoff: 100ms, 200ms, 300ms...
return retryCount * 100;
},
},
});Request Cancellation with AbortSignal
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
const redisWithTimeout = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
signal: () => AbortSignal.timeout(5000), // 5 second timeout per request
});
try {
await redisWithTimeout.get("key");
} catch (error) {
if (error.name === "TimeoutError") {
console.error("Request timed out");
}
}Pipeline Optimization
Overview
Pipelines batch multiple Redis commands into one HTTP request. Use automatic pipelines for Promise.all patterns, manual pipelines for sequential operations.
Good For
- Multiple independent operations
- High-latency networks
- Serverless functions with cold starts
- Operations that don't depend on each other's results
Limitations
- Commands in pipeline cannot depend on previous results
- Large pipelines increase memory usage
- Errors in one command don't stop others
Examples
Auto-Pipeline Basics
import { Redis } from "@upstash/redis";
// Auto-pipeline: enable globally
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
enableAutoPipelining: true,
});
// With auto-pipeline: Promise.all batches automatically
const [user, posts, comments] = await Promise.all([
redis.get("user:1"),
redis.lrange("posts:1", 0, 9),
redis.lrange("comments:1", 0, 9),
]);
// Single HTTP request!Manual Pipeline
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Manual pipeline: explicit control
const pipeline = redis.pipeline();
pipeline.get("user:1");
pipeline.lrange("posts:1", 0, 9);
pipeline.lrange("comments:1", 0, 9);
const results = await pipeline.exec();When to Use Pipelines
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// ❌ Bad: second operation depends on first
const bad = redis.pipeline();
bad.get("counter");
bad.incr("counter"); // Might not see previous get result
await bad.exec();
// ✅ Good: independent operations
const good = redis.pipeline();
good.get("user:1");
good.get("user:2");
good.get("user:3");
await good.exec();Pipeline Size Optimization
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
const OPTIMAL_SIZE = 50;
async function pipelinedBatch(keys: string[]) {
const results = [];
for (let i = 0; i < keys.length; i += OPTIMAL_SIZE) {
const batch = keys.slice(i, i + OPTIMAL_SIZE);
const pipeline = redis.pipeline();
batch.forEach((key) => pipeline.get(key));
const batchResults = await pipeline.exec();
results.push(...batchResults);
}
return results;
}
// Usage
const keys = Array.from({ length: 200 }, (_, i) => `key:${i}`);
const allResults = await pipelinedBatch(keys);Global Redis Replicas
Overview
Upstash Redis uses global replication with one primary and multiple replicas worldwide. Writes go to primary, reads can use nearest replica. Read-your-writes ensures consistency.
If you want to optimize read latency, you can add more read regions to your Upstash Redis database via the Upstash Console.
Good For
- Low-latency reads from nearby regions
- Global applications with distributed users
- Read-heavy workloads
- High availability
TTL and Key Expiration
Overview
Set Time-To-Live (TTL) on keys for automatic expiration. Useful for caches, sessions, and temporary data to manage memory usage.
Good For
- Cache expiration (prevent stale data)
- Session timeouts
- Temporary data storage
- Memory management
Examples
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Set with expiration (EX = seconds)
await redis.set("session:123", { userId: "user1" }, { ex: 3600 });
// Add TTL to existing key
await redis.set("key", "value");
await redis.expire("key", 300); // Expire in 5 minutes
// Get TTL of a key
const ttl = await redis.ttl("key");
console.log(`TTL: ${ttl} seconds`);
// Returns: remaining seconds, -1 (no expiry), -2 (key doesn't exist)Adapters
Overview
@upstash/redis works over HTTP, which is ideal for serverless environments. If you need TCP-based connections (e.g., long-running servers with redis or ioredis), use the adapter packages @upstash/search-redis and @upstash/search-ioredis. They provide the exact same search API.
Packages
| Package | Client | Protocol | Install |
|---|---|---|---|
@upstash/redis | Built-in | HTTP/REST | npm install @upstash/redis |
@upstash/search-redis | node-redis | TCP | npm install @upstash/search-redis redis |
@upstash/search-ioredis | ioredis | TCP | npm install @upstash/search-ioredis ioredis |
Examples
With @upstash/redis (HTTP)
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
const index = await redis.search.createIndex({
name: "products",
prefix: "product:",
dataType: "json",
schema: s.object({ name: s.string(), price: s.number("F64") }),
});
const results = await index.query({
filter: { name: { $eq: "laptop" } },
select: { name: true, price: true },
});With node-redis (TCP)
import { createClient } from "redis";
import { createSearch, s } from "@upstash/search-redis";
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
const search = createSearch(client);
const index = await search.createIndex({
name: "products",
prefix: "product:",
dataType: "json",
schema: s.object({ name: s.string(), price: s.number("F64") }),
});
const results = await index.query({
filter: { name: { $eq: "laptop" } },
select: { name: true, price: true },
});
await client.disconnect();With ioredis (TCP)
import IORedis from "ioredis";
import { createSearch, s } from "@upstash/search-ioredis";
const ioredis = new IORedis(process.env.REDIS_URL);
const search = createSearch(ioredis);
const index = await search.createIndex({
name: "products",
prefix: "product:",
dataType: "json",
schema: s.object({ name: s.string(), price: s.number("F64") }),
});
const results = await index.query({
filter: { name: { $eq: "laptop" } },
select: { name: true, price: true },
});
await ioredis.disconnect();API Parity
All three packages expose identical search APIs:
search.createIndex()/createSearch(client).createIndex()search.index()/createSearch(client).index()search.alias.list(),search.alias.add(),search.alias.delete()index.query(),index.aggregate(),index.count()index.describe(),index.drop(),index.waitIndexing()index.addAlias()
The only difference is initialization. The s schema builder is re-exported from all packages.
Aggregations
Overview
Run analytics over indexed data using metric and bucket aggregations. Compute statistics, group documents, build histograms, and perform faceted navigation. Aggregations can be nested for multi-level analysis.
Good For
- Computing averages, sums, min/max across documents
- Grouping documents by field values (category breakdown)
- Building price range facets for e-commerce
- Histogram distributions (price ranges, date ranges)
- Multi-level analytics (average price per category)
Examples
Metric Aggregations
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
const index = await redis.search.createIndex({
name: "orders",
prefix: "order:",
dataType: "json",
schema: s.object({
product: s.string(),
category: s.facet(),
price: s.number("F64"),
quantity: s.number("U64"),
date: s.date(),
}),
});
// Insert sample data
await redis.json.set("order:1", "$", {
product: "Laptop",
category: "electronics",
price: 999.99,
quantity: 1,
date: "2024-06-15",
});
await redis.json.set("order:2", "$", {
product: "Mouse",
category: "electronics",
price: 29.99,
quantity: 3,
date: "2024-06-16",
});
await redis.json.set("order:3", "$", {
product: "Desk",
category: "furniture",
price: 249.99,
quantity: 1,
date: "2024-07-01",
});
await index.waitIndexing();
// Average price
const result = await index.aggregate({
aggregations: {
avg_price: { $avg: { field: "price" } },
},
});
// result.avg_price -> number
// Multiple metrics at once
const stats = await index.aggregate({
aggregations: {
avg_price: { $avg: { field: "price" } },
total_revenue: { $sum: { field: "price" } },
cheapest: { $min: { field: "price" } },
most_expensive: { $max: { field: "price" } },
order_count: { $count: { field: "price" } },
},
});
// Combined statistics
const priceStats = await index.aggregate({
aggregations: {
price_stats: { $stats: { field: "price" } },
// Returns: { count, min, max, sum, avg }
},
});
// Extended statistics (includes variance and standard deviation)
const extended = await index.aggregate({
aggregations: {
price_extended: { $extendedStats: { field: "price" } },
// Returns: { count, min, max, sum, avg, sumOfSquares, variance, stdDeviation }
},
});
// Percentiles
const percentiles = await index.aggregate({
aggregations: {
price_percentiles: { $percentiles: { field: "price", percents: [25, 50, 75, 95] } },
},
});
// Count distinct values
const uniqueCategories = await index.aggregate({
aggregations: {
unique_cats: { $cardinality: { field: "category" } },
},
});Bucket Aggregations
$terms - Group by field values
const byCategory = await index.aggregate({
aggregations: {
categories: {
$terms: { field: "category", size: 10 },
},
},
});
// categories.buckets -> [{ key: "electronics", doc_count: 2 }, { key: "furniture", doc_count: 1 }]$range - Group by numeric ranges
const priceRanges = await index.aggregate({
aggregations: {
price_ranges: {
$range: {
field: "price",
ranges: [
{ to: 50 }, // Under $50
{ from: 50, to: 200 }, // $50-$200
{ from: 200 }, // Over $200
],
},
},
},
});$histogram - Fixed-interval numeric buckets
const priceHistogram = await index.aggregate({
aggregations: {
price_distribution: {
$histogram: { field: "price", interval: 100 },
},
},
});$facet - Faceted navigation
const facets = await index.aggregate({
aggregations: {
brand_facets: { $facet: { field: "brand" } },
},
});Nested Aggregations
Combine buckets with metrics for multi-level analysis:
// Average price per category
const result = await index.aggregate({
aggregations: {
by_category: {
$terms: { field: "category" },
$aggs: {
avg_price: { $avg: { field: "price" } },
min_price: { $min: { field: "price" } },
max_price: { $max: { field: "price" } },
total_orders: { $count: { field: "price" } },
},
},
},
});
// by_category.buckets -> [
// { key: "electronics", doc_count: 2, avg_price: 514.99, min_price: 29.99, max_price: 999.99, total_orders: 2 },
// { key: "furniture", doc_count: 1, avg_price: 249.99, ... },
// ]Filtered Aggregations
Apply a filter before aggregating:
const electronicsStats = await index.aggregate({
filter: { category: { $eq: "electronics" } },
aggregations: {
avg_price: { $avg: { field: "price" } },
price_ranges: {
$range: {
field: "price",
ranges: [{ to: 100 }, { from: 100, to: 500 }, { from: 500 }],
},
},
},
});Available Aggregations
Metric Aggregations
| Aggregation | Description |
|---|---|
$avg | Average value of a numeric field |
$sum | Sum of values |
$min | Minimum value |
$max | Maximum value |
$count | Count of documents |
$cardinality | Count of distinct values |
$stats | Combined count/min/max/sum/avg |
$extendedStats | Stats + variance/stdDeviation/sumOfSquares |
$percentiles | Percentile values at specified thresholds |
Bucket Aggregations
| Aggregation | Description |
|---|---|
$terms | Group by field values |
$range | Group by custom numeric ranges |
$histogram | Fixed-interval numeric buckets |
$facet | Faceted navigation (hierarchical) |
Aliases
Overview
Index aliases provide an indirection layer between your application and the actual index. You can point an alias to any index and swap it atomically, enabling zero-downtime reindexing.
Good For
- Zero-downtime index rebuilds (blue/green reindexing)
- A/B testing different index configurations
- Versioned index management
Examples
Add an Alias
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
// Create an index
const index = await redis.search.createIndex({
name: "products-v1",
prefix: "product:",
dataType: "json",
schema: s.object({ name: s.string(), price: s.number("F64") }),
});
// Add alias via the index instance
await index.addAlias({ alias: "products" });
// Or via the redis.search.alias API
await redis.search.alias.add({ indexName: "products-v1", alias: "products" });List All Aliases
const aliases = await redis.search.alias.list();
// { "products": "products-v1", "users": "users-v2" }Delete an Alias
await redis.search.alias.delete({ alias: "products" });
// Returns 1 if deleted, 0 if alias didn't existZero-Downtime Reindexing
// 1. Create new index with updated schema
const newIndex = await redis.search.createIndex({
name: "products-v2",
prefix: "product:",
dataType: "json",
schema: s.object({
name: s.string(),
price: s.number("F64"),
description: s.string(), // new field
}),
});
// 2. Wait for new index to finish scanning existing keys
await newIndex.waitIndexing();
// 3. Atomically swap the alias to the new index
// (addAlias updates the alias if it already exists)
await redis.search.alias.add({ indexName: "products-v2", alias: "products" });
// 4. Drop the old index
const oldIndex = redis.search.index({ name: "products" });
await oldIndex.drop();Index Management
Overview
Create, inspect, and drop search indexes. Wait for indexing to complete after data changes. Indexes automatically track Redis keys matching a specified prefix.
Good For
- Creating indexes over existing or new Redis data
- Inspecting index schema and configuration
- Rebuilding or dropping indexes
- Ensuring data consistency after bulk writes
Examples
Create an Index
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
// JSON index with nested schema
const index = await redis.search.createIndex({
name: "products",
prefix: "product:",
dataType: "json",
schema: s.object({
name: s.string(),
price: s.number("F64"),
metadata: s.object({
brand: s.facet(),
tags: s.keyword(),
}),
}),
});
// Hash index (flat schema only)
const hashIndex = await redis.search.createIndex({
name: "sessions",
prefix: "session:",
dataType: "hash",
schema: {
userId: { type: "TEXT" as const },
lastActive: { type: "DATE" as const },
},
});Create with Options
const index = await redis.search.createIndex({
name: "articles",
prefix: ["article:", "post:"], // multiple prefixes
dataType: "json",
language: "english", // stemming language
skipInitialScan: false, // scan existing keys (default)
existsOk: true, // don't error if index already exists
schema: s.object({
title: s.string(),
body: s.string().noStem(),
publishedAt: s.date().fast(),
}),
});Get a Reference to an Existing Index
// If you already created the index and just need a reference
const index = redis.search.index({
name: "products",
schema: s.object({
name: s.string(),
price: s.number("F64"),
}),
});
// Without schema (untyped - no filter/select type safety)
const untypedIndex = redis.search.index({ name: "products" });Describe an Index
const description = await index.describe();
// {
// name: "products",
// dataType: "json",
// prefixes: ["product:"],
// language: "english",
// schema: { name: { type: "TEXT" }, price: { type: "F64", fast: true } }
// }
// Returns null if index doesn't exist
const missing = await redis.search.index({ name: "nonexistent" }).describe();
// nullWait for Indexing
// After inserting/updating/deleting data, wait for index to catch up
await redis.json.set("product:1", "$", { name: "Laptop", price: 999 });
await redis.json.set("product:2", "$", { name: "Mouse", price: 29 });
await redis.json.set("product:3", "$", { name: "Keyboard", price: 79 });
await index.waitIndexing(); // blocks until all pending docs are indexed
// Queries now reflect the latest data
const results = await index.query({ filter: { name: { $eq: "Laptop" } } });Drop an Index
const result = await index.drop();
// 1 if dropped, 0 if index didn't existSupported Languages
For stemming: english, french, spanish, portuguese, italian, german, dutch, swedish, norwegian, danish, finnish, hungarian, russian, romanian, turkish, arabic, chinese, japanese
Querying & Counting
Overview
Query documents from a search index using type-safe filters with support for pagination, sorting, field selection, scoring, and highlighting. Count matching documents efficiently without returning results.
Good For
- Full-text search with fuzzy matching and phrase queries
- Filtering by numeric ranges, dates, booleans, keywords
- Paginated results with sorting
- Highlighting search terms in results
- Counting documents matching a filter
Examples
Basic Query
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
const index = await redis.search.createIndex({
name: "products",
prefix: "product:",
dataType: "json",
schema: s.object({
name: s.string(),
price: s.number("F64"),
category: s.keyword(),
inStock: s.boolean(),
}),
});
// Insert data
await redis.json.set("product:1", "$", {
name: "Gaming Laptop",
price: 1299.99,
category: "electronics",
inStock: true,
});
await redis.json.set("product:2", "$", {
name: "Wireless Mouse",
price: 29.99,
category: "electronics",
inStock: true,
});
await redis.json.set("product:3", "$", {
name: "Laptop Stand",
price: 49.99,
category: "accessories",
inStock: false,
});
await index.waitIndexing();
// Query with filter and return data
const results = await index.query({
filter: { category: { $eq: "electronics" } },
select: { name: true, price: true },
});
// [
// { key: "product:1", score: ..., data: { name: "Gaming Laptop", price: 1299.99 } },
// { key: "product:2", score: ..., data: { name: "Wireless Mouse", price: 29.99 } },
// ]Keys Only (No Data)
// Set select to {}
const keysOnly = await index.query({
filter: { inStock: { $eq: true } },
select: {},
});
// [{ key: "product:1", score: ... }, { key: "product:2", score: ... }]Pagination
const page2 = await index.query({
filter: { category: { $eq: "electronics" } },
select: { name: true },
limit: 10,
offset: 10, // skip first 10 results
});Sorting
const cheapest = await index.query({
filter: { inStock: { $eq: true } },
select: { name: true, price: true },
orderBy: { price: "ASC" },
});Score Function
// Rank by relevance with a score modifier
const results = await index.query({
filter: { name: { $eq: "laptop" } },
select: { name: true },
scoreFunc: { field: "name", modifier: "LOG1P" },
});
// Available modifiers: LOG, LOG1P, LOG2P, LN, LN1P, LN2P, SQRT, SQUARE, RECIPROCAL, NONEHighlighting
const highlighted = await index.query({
filter: { name: { $eq: "laptop" } },
select: { name: true },
highlight: {
fields: ["name"],
preTag: "<mark>", // optional, default <em>
postTag: "</mark>", // optional, default </em>
},
});
// data.name: "Gaming <mark>Laptop</mark>"Filter Operators
Text Field Filters
// Exact substring match
{ name: { $eq: "laptop" } }
// Multiple values (OR)
{ name: { $in: ["laptop", "tablet"] } }
// Fuzzy matching (typo tolerance)
{ name: { $fuzzy: { term: "lapto", distance: 1 } } }
// Phrase matching (adjacent words with tolerance)
{ name: { $phrase: { text: "gaming laptop", slop: 1 } } }
// Regex pattern
{ name: { $regex: "lap.*" } }
// Smart matching (automatic fuzzy + phrase + term)
{ name: { $smart: "gaming laptop" } }Numeric Field Filters
// Exact value
{ price: { $eq: 29.99 } }
// Range
{ price: { $gte: 10, $lte: 100 } }
// Greater/less than
{ price: { $gt: 50 } }
{ stock: { $lt: 10 } }Boolean Field Filters
{
inStock: {
$eq: true;
}
}
{
inStock: {
$in: [true, false];
}
}Date Field Filters
{ createdAt: { $gte: "2024-01-01", $lt: "2025-01-01" } }Keyword Field Filters
// Exact match
{ category: { $eq: "electronics" } }
// Multiple values
{ category: { $in: ["electronics", "accessories"] } }
// Lexicographic range
{ category: { $gte: "a", $lt: "m" } }Facet Field Filters
{
brand: {
$eq: "Apple";
}
}
{
brand: {
$in: ["Apple", "Samsung"];
}
}Boolean Operators
Combine filters using boolean operators:
// AND - all conditions must match
{
$and: [
{ category: { $eq: "electronics" } },
{ price: { $lte: 500 } },
]
}
// OR - any condition matches
{
$or: [
{ category: { $eq: "electronics" } },
{ category: { $eq: "accessories" } },
]
}
// MUST + MUST NOT - require some, exclude others
{
$must: [{ category: { $eq: "electronics" } }],
$mustNot: [{ inStock: { $eq: false } }],
}
// MUST + SHOULD - required conditions + optional boosters
{
$must: [{ category: { $eq: "electronics" } }],
$should: [{ name: { $eq: "premium" } }], // boosts score if matched
}
// SHOULD alone - at least one must match (acts like OR)
{
$should: [
{ name: { $eq: "laptop" } },
{ name: { $eq: "tablet" } },
]
}Boosting
Boost the score of specific conditions:
{
$must: [
{ name: { $eq: "laptop", $boost: 2.0 } }, // double the score weight
{ category: { $eq: "electronics", $boost: 0.5 } },
];
}Counting
Count matching documents without returning them:
const { count } = await index.count({
filter: { category: { $eq: "electronics" } },
});
// count: 2Redis Search
Overview
Redis Search is a full-text search and secondary indexing extension for Upstash Redis. It provides powerful APIs for querying, filtering, and aggregating data stored in Redis keys. Built on Tantivy, it supports text search with stemming, fuzzy matching, faceted navigation, and complex aggregations.
Good For
- Full-text search over Redis data (strings, JSON, hashes)
- Filtering and sorting with type-safe queries
- Aggregations and analytics (averages, histograms, facets)
- Autocomplete and typo-tolerant search
- Faceted navigation (e-commerce categories, filters)
Packages
@upstash/redis- The primary SDK. Access search viaredis.search(works over HTTP)@upstash/search-redis- Adapter for theredis(node-redis) TCP client@upstash/search-ioredis- Adapter for theioredisTCP client
All three packages expose the same search API. See adapters.md for TCP client setup.
Schema
Schemas define which fields are indexed and how. Use the s schema builder for type-safe definitions:
import { Redis, s } from "@upstash/redis";
const redis = Redis.fromEnv();
const index = await redis.search.createIndex({
name: "products",
prefix: "product:",
dataType: "json",
schema: s.object({
name: s.string(), // TEXT - full-text searchable
description: s.string().noStem(), // TEXT without stemming
sku: s.string().noTokenize(), // TEXT stored as-is (no splitting)
price: s.number("F64"), // floating point number
stock: s.number("U64"), // unsigned 64-bit integer
inStock: s.boolean(), // boolean
createdAt: s.date(), // date
category: s.keyword(), // exact-match keyword
brand: s.facet(), // facet for aggregations
}),
});Field Types
| Builder | Redis Type | TypeScript | Use Case |
|---|---|---|---|
s.string() | TEXT | string | Full-text searchable text |
s.number() | F64/U64/I64 | number | Numeric values, ranges |
s.boolean() | BOOL | boolean | True/false filtering |
s.date() | DATE | string | Date range queries |
s.keyword() | KEYWORD | string | Exact match, lexicographic range |
s.facet() | FACET | string | Faceted aggregations |
s.object({}) | (nested) | object | Nested field groups |
Field Options
.noTokenize()- (TEXT only) Don't split on whitespace/punctuation. Use for SKUs, URLs, emails.noStem()- (TEXT only) Don't reduce words to stems. Use for brand names, proper nouns.fast()- (BOOL, DATE) Enable fast filtering.from("fieldName")- Map index field to a different field name in the stored data
Data Types
"json"- Index JSON documents stored withredis.json.set()orredis.set(). Supports nested schemas withs.object()"string"- Index JSON strings stored withredis.set(). Supports nested schemas"hash"- Index Redis hashes stored withredis.hset(). Flat schemas only (no nesting)
Commands
For detailed usage of each command category, see:
- commands/querying.md - Query and count documents with filters, pagination, sorting, highlighting
- commands/aggregating.md - Aggregations: metrics ($avg, $sum, $min, $max), buckets ($terms, $range, $histogram), facets
- commands/index-management.md - Create, describe, drop indexes; wait for indexing
- commands/aliases.md - Manage index aliases for zero-downtime reindexing
Pitfalls
Data is upserted with regular Redis commands, not through search
There is no index.upsert() or index.add() method. You store data using standard Redis commands (set, json.set, hset), and the search index automatically picks up keys matching its prefix.
// Create the index
const index = await redis.search.createIndex({
name: "users",
prefix: "user:",
dataType: "json",
schema: s.object({ name: s.string(), age: s.number("U64") }),
});
// Upsert data with regular Redis commands
await redis.json.set("user:1", "$", { name: "Alice", age: 30 });
await redis.json.set("user:2", "$", { name: "Bob", age: 25 });
// Wait for the index to process the new data
await index.waitIndexing();
// Now you can query
const results = await index.query({
filter: { name: { $eq: "Alice" } },
});Always call waitIndexing after data changes
Index updates are batched. After upserting or deleting data, the index may not immediately reflect the changes. Call waitIndexing() to block until all pending documents are processed.
// Batch upsert many documents
for (const product of products) {
await redis.json.set(`product:${product.id}`, "$", product);
}
// Call waitIndexing ONCE after all upserts (not after each one)
await index.waitIndexing();
// Now queries will return up-to-date results
const results = await index.query({ filter: { category: { $eq: "electronics" } } });Tokenization splits text at word boundaries
TEXT fields are tokenized by default: "hello-world" becomes ["hello", "world"]. An $eq filter for "hello-world" matches because it finds the substring. But if you need exact matching of the full string (e.g., SKUs, URLs), use .noTokenize().
Stemming reduces words to roots
By default, TEXT fields apply language-specific stemming: "running" is stored as "run". This means $regex patterns won't match the original form. Disable with .noStem() for brand names or when you need exact word forms.
$mustNot cannot be used alone
$mustNot filters only exclude documents. Using $mustNot alone returns no results. Always combine it with $must or $should:
// Won't work - returns nothing
{ $mustNot: [{ status: { $eq: "archived" } }] }
// Correct - exclude within a broader match
{ $must: [{ category: { $eq: "electronics" } }], $mustNot: [{ status: { $eq: "archived" } }] }SCOREFUNC and ORDERBY are mutually exclusive
You cannot use scoreFunc and orderBy in the same query. Use orderBy for deterministic sorting, scoreFunc for relevance-based ranking.
Resources
Related skills
How it compares
Pick redis-js over generic Redis skills when the stack uses Upstash serverless HTTP Redis rather than self-hosted TCP Redis clusters.
FAQ
What does the redis-js skill cover?
redis-js covers the @upstash/redis JavaScript/TypeScript SDK for caching, session storage, rate limiting, leaderboards, full-text search, and all Redis data structures. It includes 23 topic guides with automatic serialization and migration paths from ioredis.
Does redis-js document common Redis SDK mistakes?
redis-js documents common LLM mistakes such as treating all values as strings and manually JSON-serializing objects. The @upstash/redis SDK preserves JavaScript types automatically, and the skill shows correct patterns per data structure.