@upstash/redis
A TypeScript Redis client that talks to Upstash's REST API over HTTP instead of opening a TCP connection with the Redis protocol. Every command returns a promise, which makes the client usable in serverless functions, edge runtimes, workers, WebAssembly and ordinary Node processes. It covers common strings, hashes, lists, sets, sorted sets, streams, scripts, JSON and search operations, plus HTTP-efficient pipelines and atomic multi-command transactions.
An excellent fit for Upstash from serverless and edge runtimes, where HTTP is a feature rather than a compromise. For a long-running Node service talking to ordinary Redis, use a TCP client and avoid the vendor-specific endpoint, token and command subset.
Use it if
- You use an Upstash Redis database from serverless or edge code where long-lived TCP sockets are unavailable or inconvenient
- You want one typed client with dedicated Node, Cloudflare and Fastly entry points over the platform's fetch implementation
- Your workload can batch independent commands into one HTTP pipeline or needs an isolated multi transaction
- You want automatic object serialization, optional read-your-writes behavior and an API close to familiar Redis commands
- You connect to self-hosted Redis, AWS ElastiCache or another RESP endpoint: this client requires the Upstash REST URL and token, not a redis:// connection string
- You have a hot, chatty Node service beside Redis and need minimum per-command latency: a pooled TCP client such as redis or ioredis avoids HTTP request overhead
- You rely on the complete Redis command surface or modules without checking compatibility: the README links a specific supported-command list and the declarations do not expose commands such as WATCH
- You plan to ship a full-access REST token in browser code: supporting browsers does not make an embedded database credential secret, and anyone receiving the bundle can reuse it
- You assume retries make non-idempotent writes exactly once: the client defaults to network retries, so a lost response around INCR, LPUSH or another mutation needs application-level idempotency
Setup reality
Create an Upstash Redis database and provide UPSTASH_REDIS_REST_URL plus UPSTASH_REDIS_REST_TOKEN. Redis.fromEnv() reads those names in Node and falls back to KV_REST_API_URL and KV_REST_API_TOKEN for compatible Vercel setups; workers usually receive bindings explicitly and Cloudflare has a dedicated export. The package has one runtime dependency and no native build, TCP socket or connection pool. Every normal command is an HTTP round trip, so deploy compute near the database and use pipeline(), multi() or opt-in automatic pipelining when several operations can travel together. Pipelines preserve command order but are not atomic; multi transactions are isolated. The requester retries network failures five times by default with exponential backoff, which improves reads but can repeat a mutation when the server applied it and the response was lost. Configure retry and idempotency deliberately. Responses use base64 encoding by default to preserve non-UTF-8 data, and values are automatically deserialized by default; a TypeScript get<T>() annotation does not validate stored data at runtime. The request cache defaults to no-store. Enable readYourWrites when subsequent reads from the same client must observe earlier writes. The SDK emits anonymous runtime telemetry unless UPSTASH_DISABLE_TELEMETRY is truthy or enableTelemetry is false. Finally, use an AbortSignal for request deadlines, keep credentials server-side where possible, and remember that HTTP compatibility is not the same as access to every Redis server or command.
Patterns
Create a Node client from environment variablesconnect-from-env
import { Redis } from '@upstash/redis';
export const redis = Redis.fromEnv({
enableTelemetry: false,
readYourWrites: true,
});
await redis.ping();fromEnv expects UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN, with KV_REST_API_URL and KV_REST_API_TOKEN as fallbacks. Missing values fail at runtime.
Configure credentials, retries and a deadlineconfigure-client
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
signal: () => AbortSignal.timeout(1500),
retry: { retries: 2, backoff: (attempt) => 50 * 2 ** attempt },
enableTelemetry: false,
});A signal factory creates a fresh deadline per request. Retrying a mutation can repeat it after an ambiguous network failure, so keep non-idempotent flows guarded.
Store and retrieve a typed object with TTLcache-json-value
type Session = { userId: string; roles: string[] };
await redis.set('session:abc', { userId: 'u1', roles: ['editor'] }, { ex: 900 });
const session = await redis.get<Session>('session:abc');Objects are serialized and deserialized automatically, but get<Session> is only a compile-time assertion. Validate untrusted or long-lived stored data at runtime.
Claim a key only when it does not existset-if-absent
const claimed = await redis.set(
'job-lock:42',
crypto.randomUUID(),
{ nx: true, ex: 30 },
);
if (claimed !== 'OK') {
throw new Error('Job is already claimed');
}A simple expiring lock does not prove ownership during release or protect work that outlives the TTL. Use a token-checked Lua release for real distributed locking.
Store and read hash fieldswork-with-hash
await redis.hset('user:42', {
name: 'Ada',
plan: 'pro',
logins: 7,
});
const user = await redis.hgetall<{
name: string;
plan: string;
logins: number;
}>('user:42');The generic shapes the TypeScript result but does not enforce a schema in Redis. Mixed writers can still store missing fields or incompatible values.
Maintain and query a leaderboarduse-sorted-set
await redis.zadd('scores', { score: 1200, member: 'team:blue' });
await redis.zadd('scores', { score: 980, member: 'team:red' });
const leaders = await redis.zrange<string[]>('scores', 0, 9, {
rev: true,
withScores: true,
});withScores changes the returned shape to alternating members and scores. Confirm the generic against your chosen options instead of assuming it returns objects.
Batch independent commands into one HTTP requestpipeline-commands
const [setResult, profile, count] = await redis
.pipeline()
.set('seen:u1', true, { ex: 3600 })
.get<{ name: string }>('profile:u1')
.incr('page-views')
.exec();A pipeline reduces HTTP round trips and executes commands in order, but other clients can interleave operations. It is not atomic.
Execute commands as an isolated transactionrun-transaction
const [remaining, recorded] = await redis
.multi()
.decrby('inventory:sku-7', 1)
.lpush('orders:pending', 'order-99')
.exec();multi is atomic and isolated, but it does not add application invariants by itself. This example can make inventory negative unless a Lua script checks the value before decrementing.
Keep per-command errors in a pipelinehandle-pipeline-errors
const results = await redis
.pipeline()
.get('good-key')
.incr('possibly-wrong-type')
.exec({ keepErrors: true });
for (const item of results) {
if (item.error) console.error(item.error);
}Without keepErrors, one command error rejects the whole pipeline result. Earlier commands may already have executed, so rejection does not mean rollback.
Iterate matching keys without KEYSscan-keyspace
let cursor = '0';
do {
const [next, keys] = await redis.scan(cursor, {
match: 'session:*',
count: 200,
});
cursor = next;
for (const key of keys) await processKey(key);
} while (cursor !== '0');SCAN can return an empty page before the cursor reaches zero and may see duplicates while the keyspace changes. Make processing idempotent.
Store and update Redis JSONupdate-json-document
await redis.json.set('cart:u1', '$', {
items: [],
total: 0,
});
await redis.json.arrappend('cart:u1', '$.items', { sku: 'A1', qty: 2 });
const cart = await redis.json.get<{ items: unknown[]; total: number }>('cart:u1');JSON commands depend on Upstash's supported Redis JSON surface. JSONPath results can have array-shaped semantics, so test the exact path and return type you use.
Apply a conditional update with Luarun-lua-script
const result = await redis.eval<[], number>(
`local n = tonumber(redis.call('GET', KEYS[1]) or '0')
if n <= 0 then return 0 end
redis.call('DECR', KEYS[1])
return 1`,
['inventory:sku-7'],
[],
);Keep KEYS and ARGV separate from script text. The generic describes the expected response but does not validate what the script actually returns.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| redis | npm | Choose the official Node Redis client for TCP connections to self-hosted Redis, ElastiCache or any standard RESP server |
| ioredis | npm | Choose it for established Node services that need Redis Cluster, Sentinel, offline queues and connection-oriented behavior |
| keyv | npm | Choose it when you only need a portable key-value cache API and want the option to switch storage adapters |