mrkeyoor.com_
Sat 08 Aug 21:01 UTC
npmDataupdated 08 Aug 2026

@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.

Verdict

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.

API stability4/5Version 1.x keeps familiar lowercase Redis commands, a constructor or fromEnv factory, pipelines and multi transactions, and the generated declarations strongly type option combinations such as mutually exclusive SET expirations and NX versus XX. New REST, search and runtime features continue to arrive, so the surface is not frozen. Because the endpoint is Upstash-specific, server capability changes and SDK changes must be considered together even when ordinary command signatures stay stable.
Docs4/5The README gives an accurate positioning statement, runtime list, install path, credential setup, basic examples, troubleshooting link, supported-command link and a clear telemetry disclosure. The dedicated Upstash documentation covers commands and platform integrations. Important behavior such as five default retries, base64 response encoding, no-store caching, automatic deserialization, pipeline error modes and read-your-writes is easiest to discover in declarations or deeper reference pages rather than the quick start.
Maintenance5/5Version 1.38.2 was published on August 4, 2026 and the repository was pushed on August 7. The README labels the SDK GA, promises professional support and explains a changeset-based stable and canary release workflow. Pull requests run unit tests, lint, builds and integration examples, with additional scheduled tests. GitHub reported 15 combined open issues and pull requests, and current releases include telemetry fixes rather than only automated dependency churn.
Ecosystem4/5The package recorded 4,549,163 downloads in the measured week and ships explicit Node, Cloudflare and Fastly entry points while also documenting Deno, Lambda, Next.js, workers, mobile and WebAssembly use. Its command names and data structures are recognizable to Redis users. The boundary is vendor coupling: only Upstash's REST API accepts this transport, and standard Redis clients, modules and operational tools do not become interchangeable merely because commands look alike.

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
Skip it if

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

PackageRegistryPick it when
redisnpmChoose the official Node Redis client for TCP connections to self-hosted Redis, ElastiCache or any standard RESP server
ioredisnpmChoose it for established Node services that need Redis Cluster, Sentinel, offline queues and connection-oriented behavior
keyvnpmChoose it when you only need a portable key-value cache API and want the option to switch storage adapters