
Upstash
- 1 installs
- 2 repo stars
- Updated August 3, 2026
- fandhe-ai/agent-reference-skills
Reference the Upstash (QStash) messaging API for publishing and batching messages to workers and URL groups with per-message options.
About
A structured reference for the Upstash QStash messaging API covering publishJSON, batchJSON, and per-message options for sending messages to workers and URL groups. A backend developer loads it when integrating Upstash message queues.
- batchJSON publishes multiple independent messages in one call
- Per-message options mirror publishJSON
Upstash by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,366 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fandhe-ai/agent-reference-skills --skill upstashAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | fandhe-ai/agent-reference-skills ↗ |
What it does
Reference the Upstash (QStash) messaging API for publishing and batching messages to workers and URL groups with per-message options.
Files
batchJSON
Publish multiple messages in a single API call. Each message is independent—if one fails, others still process.
Signature / Usage
const res = await client.batchJSON([
{ url: "https://example.com/worker1", body: { task: "a" } },
{ url: "https://example.com/worker2", body: { task: "b" } },
{ urlGroup: "my-group", body: { broadcast: true } },
]);Options / Props
Each element of the array accepts the same options as publishJSON:
| Name | Type | Description |
|---|---|---|
url | string | Destination URL (mutually exclusive with urlGroup) |
urlGroup | string | URL Group name for fan-out delivery |
body | unknown | Message payload (serialized to JSON) |
headers | Record<string, string> | Per-message custom headers |
delay | `number \ | string` |
callback | string | Per-message success/failure callback URL |
failureCallback | string | Per-message failure-only callback URL |
Notes
- Returns an array of responses, one per message in input order
- URL Group entries expand to an array of responses (one per subscribed endpoint)
- Can mix URLs, URL Groups, and queue targets in one batch request
Related
- publish.md
- url-groups.md
- queues.md
Callbacks
Receive delivery results at a specified URL. callback fires on every delivery attempt result; failureCallback fires only after all retries are exhausted.
Signature / Usage
await client.publishJSON({
url: "https://my-api.example.com/handler",
body: { hello: "world" },
callback: "https://my-api.example.com/on-delivered",
failureCallback: "https://my-api.example.com/on-failed",
});Options / Props
Publish options:
| Name | Type | Description |
|---|---|---|
callback | string | URL that receives a POST with delivery result on every attempt |
failureCallback | string | URL that receives a POST only after all retries are exhausted |
Callback request body (JSON):
| Field | Type | Description |
|---|---|---|
status | number | HTTP response code returned by destination |
body | string | Base64-encoded response body from destination |
header | Record<string, string[]> | Response headers from destination |
retried | number | Number of retry attempts made |
maxRetries | number | Maximum configured retry count |
sourceMessageId | string | ID of the original published message |
url | string | Destination URL that was called |
method | string | HTTP method used |
dlqId | string | DLQ message ID (present in failureCallback only) |
createdAt | number | Unix timestamp when message was created |
notBefore | number | Unix timestamp before which delivery was not attempted |
topicName | string | URL Group name (if published to a URL Group) |
scheduleId | string | Schedule ID (if triggered by a schedule) |
callerIP | string | IP address of the original publisher |
Notes
- Callback endpoints are themselves delivered via QStash and support the same per-callback configuration headers:
Upstash-Callback-Timeout,Upstash-Callback-Retries,Upstash-Callback-Delay,Upstash-Callback-Method - Use
Upstash-Callback-Forward-<Header>to attach custom headers to callback deliveries - Apply the same prefix pattern for failure callbacks:
Upstash-Failure-Callback-* - Decode
bodywithBuffer.from(body, "base64").toString()oratob(body)
Related
- publish.md
- dlq.md
Dead Letter Queue (DLQ)
Messages that exhaust all delivery retries are automatically moved to the Dead Letter Queue. DLQ messages can be retried or deleted individually or in bulk.
Signature / Usage
// List DLQ messages (paginated)
const res = await client.dlq.listMessages({
count: 10,
order: "latestFirst",
filter: { url: "https://example.com" },
});
// res.messages — array of DLQ message objects
// res.cursor — pass to next call for pagination
// Retry specific messages
await client.dlq.retry(["dlq-id-1", "dlq-id-2"]);
// Delete specific messages
await client.dlq.delete(["dlq-id-1", "dlq-id-2"]);
// Bulk delete with filter
await client.dlq.delete({
filter: { url: "https://example.com", responseStatus: 500 },
all: true,
});Options / Props
`dlq.listMessages()` options:
| Name | Type | Description |
|---|---|---|
cursor | string | Pagination cursor from previous response |
count | number | Number of messages to return per page |
order | "latestFirst" | Sort order |
filter | { url?: string; responseStatus?: number } | Filter messages by destination URL or HTTP response status |
dlqIds | string[] | Fetch specific DLQ message IDs |
`dlq.retry()` / `dlq.delete()` options:
| Name | Type | Description |
|---|---|---|
| (positional) | string | Single DLQ message ID |
| (positional) | string[] | Multiple DLQ message IDs |
filter | { url?: string; queueName?: string; responseStatus?: number } | Bulk operation filter |
cursor | string | Pagination cursor for large bulk operations |
all | boolean | Apply operation to all matching messages |
Notes
- Retried DLQ messages re-enter the delivery pipeline with the same retry configuration as new messages
- DLQ message retention is determined by your Upstash subscription tier
- Messages are permanently removed when their retention period expires
failureCallbackinpublishJSONfires when a message is moved to DLQ
Related
- publish.md
- callbacks.md
- queues.md
Flow Control
Rate limiting and parallelism control for message delivery. Group messages under a named key and configure how many can be delivered per time window and how many can be active concurrently.
Signature / Usage
await client.publishJSON({
url: "https://example.com/api/handler",
body: { hello: "world" },
flowControl: {
key: "per-user-rate-limit",
rate: 100,
period: "1m",
parallelism: 5,
},
});Options / Props
`flowControl` object in `publishJSON` / `enqueueJSON`:
| Name | Type | Default | Description |
|---|---|---|---|
key | string | — | User-defined identifier grouping messages for shared limits |
rate | number | — | Maximum number of deliveries allowed within period |
period | string | "1s" | Time window for rate limit (e.g. "1s", "1m", "1h") |
parallelism | number | — | Maximum number of concurrently active (in-flight) delivery requests |
Notes
- Messages that exceed rate or parallelism limits are queued automatically and delivered once constraints clear
- Flow control state is tracked server-side per
key - The Management API provides endpoints to: get key metrics, pause/resume delivery for a key, pin configurations, reset rate period counters, and monitor global parallelism
parallelisminflowControlapplies across all messages sharing the samekey; queue-levelparallelism(inqueue.upsert()) applies only within that queue
Related
- publish.md
- queues.md
Messages
Retrieve and cancel in-flight messages by ID.
Signature / Usage
// Get a message by ID
const msg = await client.messages.get("message-id");
// Cancel a single message
await client.messages.cancel("message-id");
// Cancel multiple messages
await client.messages.cancel(["message-id-1", "message-id-2"]);
// Cancel all messages
let result = { cancelled: Infinity };
while (result.cancelled > 0) {
result = await client.messages.cancel({ all: true });
}
// Cancel by filter
await client.messages.cancel({ filter: { url: "https://example.com" } });Options / Props
`messages.cancel()` overloads:
| Signature | Description |
|---|---|
cancel(id: string) | Cancel a single message |
cancel(ids: string[]) | Cancel multiple messages by ID |
cancel({ all: true }) | Cancel all messages; returns { cancelled: number } |
cancel({ filter: { url?, label? } }) | Cancel messages matching filter; returns { cancelled: number } |
Notes
messages.get()may returnnullfor messages that have already been delivered—messages are removed from the database shortly after deliverydelete(),deleteMany(), anddeleteAll()are deprecated; usecancel()in all casescancel({ all: true })should be called in a loop untilcancelled === 0to handle large queues- Cancelling a message that is currently being delivered has no effect on the ongoing attempt
Related
- publish.md
- dlq.md
- queues.md
QStash Overview
Serverless messaging and scheduling solution. Acts as middleware between services—handles durable message delivery, automatic retries, scheduling, and fan-out without managing infrastructure.
Signature / Usage
import { Client } from "@upstash/qstash";
const client = new Client({ token: process.env.QSTASH_TOKEN! });Options / Props
`Client` constructor options:
| Name | Type | Default | Description |
|---|---|---|---|
token | string | — | QStash token from Upstash Console |
retry | `{ retries: number; backoff: (n: number) => number } \ | false` | 6 attempts w/ exponential backoff |
devMode | boolean | false | Starts a local QStash dev server automatically; no credentials required |
enableTelemetry | boolean | true | Sends SDK version, platform, runtime info to Upstash |
Notes
- Install:
npm install @upstash/qstash - Requires a publicly accessible HTTP endpoint to receive messages
- Message payload max size: 1 MB
- Supported payload formats: JSON, XML, binary, plain text
- Get
QSTASH_TOKEN,QSTASH_CURRENT_SIGNING_KEY, andQSTASH_NEXT_SIGNING_KEYfrom the Upstash Console - Opt out of telemetry via
UPSTASH_DISABLE_TELEMETRY=1env var orenableTelemetry: false
Related
- publish.md
- receiver.md
publishJSON / publish
Publish a single message to a URL or URL Group. publishJSON serializes the body as JSON and sets Content-Type: application/json automatically.
Signature / Usage
const res = await client.publishJSON({
url: "https://example.com/api/handler",
body: { hello: "world" },
});
// res.messageId — track delivery in Upstash ConsoleOptions / Props
| Name | Type | Default | Description |
|---|---|---|---|
url | string | — | Destination endpoint URL (mutually exclusive with urlGroup) |
urlGroup | string | — | URL Group name; fans out to all subscribed endpoints |
body | unknown | — | Message payload; serialized to JSON by publishJSON |
headers | Record<string, string> | — | Custom headers forwarded to destination (sent as Upstash-Forward-*) |
method | string | "POST" | HTTP method used when delivering the message |
delay | `number \ | string` | — |
notBefore | number | — | Absolute Unix timestamp (seconds, UTC) before which message is not delivered; takes precedence over delay |
retries | number | plan default | Max delivery attempts; cannot exceed plan limit |
retryDelay | string | — | Custom backoff expression using retried variable (e.g. "pow(2, retried) * 1000") |
callback | string | — | URL to receive delivery result (success or failure) |
failureCallback | string | — | URL to receive result only after all retries are exhausted |
deduplicationId | string | — | Explicit deduplication key; duplicate within 10-minute window returns 202 with existing ID |
contentBasedDeduplication | boolean | false | Auto-generates dedup ID from destination + body + headers |
timeout | string | — | Per-delivery request timeout (e.g. "30s") |
flowControl | { key: string; rate?: number; period?: string; parallelism?: number } | — | Rate limiting and concurrency control |
Notes
- Returns
{ messageId: string }on success - Duplicate detected: HTTP
202with originalmessageId; message not re-enqueued - Deduplication window: 10 minutes
- Default exponential backoff:
min(86400, e^(2.5*n))seconds; capped at 24 h after attempt 5+ - Return
489withUpstash-NonRetryable-Error: trueto skip remaining retries immediately
Related
- batch.md
- schedules.md
- queues.md
- callbacks.md
- dlq.md
Queues
FIFO ordered message delivery. Messages in a queue are processed one at a time (default parallelism: 1); the next message waits until the current delivery—including callbacks and retries—completes.
Signature / Usage
const queue = client.queue({ queueName: "my-queue" });
// Enqueue a message
await queue.enqueueJSON({
url: "https://example.com/api/handler",
body: { task: "process" },
});
// Create / update queue with parallelism
await queue.upsert({ parallelism: 2 });
// Inspect queue
const details = await queue.get();
// Pause and resume processing
await queue.pause();
await queue.resume();
// Delete queue
await queue.delete();Options / Props
`client.queue()` constructor:
| Name | Type | Description |
|---|---|---|
queueName | string | Identifies the queue to operate on |
`queue.upsert()` options:
| Name | Type | Default | Description |
|---|---|---|---|
parallelism | number | 1 | Number of messages processed concurrently |
`queue.enqueueJSON()` options:
Accepts the same options as publishJSON (body, headers, delay, retries, callback, failureCallback, deduplicationId, etc.) plus:
| Name | Type | Description |
|---|---|---|
url | string | Destination endpoint for this message |
Notes
- Messages are delivered in FIFO order within a queue
parallelism > 1enables concurrent delivery while preserving enqueue orderpause()/resume()may take up to 1 minute to take effect; avoid concurrent callsqueue.get()returns queue configuration including currentparallelismsetting- Schedules can target a queue via the
queueNameoption inschedules.create()
Related
- publish.md
- schedules.md
- dlq.md
qstash
| Name | Description | Path |
|---|---|---|
| Overview | SDK installation, Client constructor options, environment variables | overview.md |
| publishJSON / publish | Publish a single message to a URL or URL Group with all delivery options | publish.md |
| batchJSON | Publish multiple messages in one API call; mix URLs, URL Groups, and queues | batch.md |
| Schedules | Create and manage cron-based recurring message delivery | schedules.md |
| Queues | FIFO ordered delivery with configurable parallelism; enqueue, pause, resume | queues.md |
| Dead Letter Queue (DLQ) | List, retry, and delete messages that exhausted all delivery retries | dlq.md |
| Callbacks | Receive delivery results (callback) or failure notifications (failureCallback) at a URL | callbacks.md |
| URL Groups | Fan-out: publish once to deliver to multiple subscribed endpoints | url-groups.md |
| Receiver | Verify incoming requests are signed by QStash (Receiver.verify, Next.js helpers) | receiver.md |
| Messages | Retrieve and cancel in-flight messages by ID | messages.md |
| Flow Control | Rate limiting and concurrency control per named key | flow-control.md |
Receiver
Verifies that incoming HTTP requests originate from QStash. Uses HMAC SHA-256 signed JWTs sent in the Upstash-Signature header. Supports automatic key rotation via currentSigningKey / nextSigningKey.
Signature / Usage
import { Receiver } from "@upstash/qstash";
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
// In your request handler:
const isValid = await receiver.verify({
body: rawBodyString, // must be the unparsed raw string
signature: req.headers["upstash-signature"],
url: "https://your-api.example.com/handler",
});Next.js App Router helper (wraps verify automatically):
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
async function handler(req: Request) {
const payload = await req.json();
// ...
return Response.json({ ok: true });
}
export const POST = verifySignatureAppRouter(handler);Options / Props
`Receiver` constructor:
| Name | Type | Description |
|---|---|---|
currentSigningKey | string | Active HMAC signing key from Upstash Console |
nextSigningKey | string | Next signing key; used as fallback during key rotation |
`receiver.verify()` options:
| Name | Type | Description |
|---|---|---|
body | string | Raw (unparsed) request body string |
signature | string | Value of Upstash-Signature request header |
url | string | Your endpoint URL; must match the JWT sub claim |
Returns: Promise<boolean> — true if signature is valid.
Notes
- Always pass the raw body string; re-serializing a parsed object may break the SHA-256 body hash check
- JWT claims verified:
iss === "Upstash",sub === url,exp/nbftimestamps,bodySHA-256 hash - JWT lifetime defaults to 5 minutes (
exp-iat) QSTASH_CURRENT_SIGNING_KEYandQSTASH_NEXT_SIGNING_KEYenv vars are automatically read by the Next.js helpers- Rotate keys in the Upstash Console;
nextSigningKeyallows zero-downtime rotation - Requests that fail verification should return
403
Related
- overview.md
- publish.md
Schedules
Create and manage cron-based recurring message delivery. QStash publishes the configured message automatically on each cron tick.
Signature / Usage
// Create a schedule
const schedule = await client.schedules.create({
destination: "https://example.com/api/cron",
cron: "0 9 * * 1-5", // weekdays at 09:00 UTC
});
// List all schedules
const all = await client.schedules.list();
// Get a specific schedule
const s = await client.schedules.get("scheduleId");
// Pause / resume
await client.schedules.pause({ schedule: "scheduleId" });
await client.schedules.resume({ schedule: "scheduleId" });
// Delete
await client.schedules.delete("scheduleId");Options / Props
`schedules.create()` options:
| Name | Type | Default | Description |
|---|---|---|---|
destination | string | — | Target URL, URL Group name/ID, or queue |
cron | string | — | Cron expression (UTC by default) |
scheduleId | string | auto-generated | Create or overwrite a schedule with this ID |
body | unknown | — | Message payload delivered on each tick |
headers | Record<string, string> | — | Headers forwarded to destination |
callback | string | — | Callback URL for each delivery result |
failureCallback | string | — | Failure callback URL |
timeout | string | — | Per-delivery request timeout |
retries | number | plan default | Max delivery attempts per tick |
Notes
- Cron expressions are evaluated in UTC by default
- Use
CRON_TZ=<IANA>prefix for other timezones:"CRON_TZ=America/New_York 0 4 * * *" - First trigger may fire up to 60 seconds after creation
- All standard
publishJSONoptions (delay, retries, etc.) apply to each scheduled delivery - Use crontab.guru to build and test expressions
Related
- publish.md
- queues.md
URL Groups
Fan-out mechanism. Publishing once to a URL Group creates an independent delivery task for each subscribed endpoint. Each task has its own retry lifecycle.
Signature / Usage
// Create a URL Group with endpoints
await client.urlGroups.addEndpoints({
name: "my-group",
endpoints: [
{ url: "https://service-a.example.com/hook" },
{ url: "https://service-b.example.com/hook" },
],
});
// Publish to the group
await client.publishJSON({
urlGroup: "my-group",
body: { event: "user.created" },
});
// Get a URL Group
const group = await client.urlGroups.get("my-group");
// List all URL Groups
const groups = await client.urlGroups.list();
// Remove endpoints from a group
await client.urlGroups.removeEndpoints({
name: "my-group",
endpoints: [{ url: "https://service-b.example.com/hook" }],
});
// Delete entire group
await client.urlGroups.delete("my-group");Options / Props
`urlGroups.addEndpoints()` options:
| Name | Type | Description |
|---|---|---|
name | string | URL Group identifier |
endpoints | Array<{ url: string }> | Endpoints to add to the group |
`urlGroups.removeEndpoints()` options:
| Name | Type | Description |
|---|---|---|
name | string | URL Group identifier |
endpoints | Array<{ url: string }> | Endpoints to remove from the group |
`urlGroups.get()` / `urlGroups.delete()` parameter:
| Name | Type | Description |
|---|---|---|
name | string | URL Group name to retrieve or delete |
Notes
urlGroups.addEndpoints()is an upsert: calling it on an existing group adds the new endpoints without removing existing onesbatchJSONresponses for URL Group entries are arrays (one response per endpoint)- Removing all endpoints from a group does not delete the group itself; use
delete()for that - URL Groups decouple producers from consumers—add/remove endpoints without changing publish code
Related
- publish.md
- batch.md
Algorithms
Three rate limiting algorithms available as static factory methods on Ratelimit (and MultiRegionRatelimit).
Signature / Usage
Fixed Window
Ratelimit.fixedWindow(tokens: number, window: Duration): AlgorithmDivides time into fixed-length windows. Counter resets when a new window begins.
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(10, "10 s"),
});Sliding Window
Ratelimit.slidingWindow(tokens: number, window: Duration): AlgorithmRolling window weighted by prior-period traffic: rate = prior × (remaining / period) + current.
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
});Token Bucket
Ratelimit.tokenBucket(refillRate: number, interval: Duration, maxTokens: number): AlgorithmBucket refills at refillRate tokens per interval; each request consumes one token.
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.tokenBucket(5, "10 s", 10),
analytics: true,
});Options / Props
fixedWindow(tokens, window)
| Name | Type | Description |
|---|---|---|
tokens | number | Maximum requests allowed per window |
window | Duration | Window duration string (e.g., "10 s", "1 m", "1 h") |
slidingWindow(tokens, window)
| Name | Type | Description |
|---|---|---|
tokens | number | Maximum requests allowed per window |
window | Duration | Rolling window duration |
tokenBucket(refillRate, interval, maxTokens)
| Name | Type | Description |
|---|---|---|
refillRate | number | Tokens added per interval |
interval | Duration | Refill interval duration |
maxTokens | number | Maximum bucket capacity |
Notes
- Fixed Window: lowest Redis command cost; susceptible to burst traffic at window boundaries
- Sliding Window: smoothest distribution; uses an approximation (assumes uniform request spread); not suitable for multi-region
- Token Bucket: allows controlled initial bursts up to
maxTokens; highest compute cost; not supported in multi-region setups Durationstrings follow"<number> <unit>"format:s(seconds),m(minutes),h(hours),d(days)- Dynamic limits (
dynamicLimits: true) are supported by all three algorithms
Related
- overview.md
- costs.md
- features.md
Redis Command Costs
Number of Redis commands consumed per operation, by algorithm. Relevant for estimating Upstash billing.
Options / Props
limit() — commands per call
| Scenario | Fixed Window | Sliding Window | Token Bucket |
|---|---|---|---|
| First call | 3 (EVAL, INCR, PEXPIRE) | 5 (EVAL, GET×2, INCR, PEXPIRE) | 4 (EVAL, HMGET, HSET, PEXPIRE) |
| Intermediate | 2 (EVAL, INCR) | 4 (EVAL, GET×2, INCR) | 4 (EVAL, HMGET, HSET, PEXPIRE) |
| Rate-limited (cache miss) | 2 (EVAL, INCR) | 3 (EVAL, GET×2) | 2 (EVAL, HMGET) |
| Rate-limited (cache hit) | 0 | 0 | 0 |
Other methods — commands per call
| Method | Fixed Window | Sliding Window | Token Bucket |
|---|---|---|---|
getRemaining() | 2 | 3 | 2 |
resetUsedTokens() | 3 | 4 | 3 |
blockUntilReady() | same as limit() | same as limit() | same as limit() |
Feature overhead (added to above per limit() call)
| Feature | Extra commands |
|---|---|
Deny lists (enableProtection: true) | +2 |
Analytics (analytics: true) | +1 |
Dynamic limits (dynamicLimits: true) | +1 (also applies to getRemaining()) |
Notes
- Cache hit (0 commands) occurs when ephemeral cache holds the blocked identifier — no Redis call is made
- Sliding Window costs are higher than Fixed Window; prefer Fixed Window if Redis command cost is a primary concern
- Token Bucket is not available in multi-region mode
Related
- algorithms.md
- features.md
- traffic-protection.md
Features
Advanced capabilities of @upstash/ratelimit beyond basic rate limiting.
Signature / Usage
Ephemeral Cache
Reduces Redis calls by caching blocked identifiers in memory. Define the Map outside the handler so it persists across warm invocations.
const cache = new Map();
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
ephemeralCache: cache,
});Pass ephemeralCache: false to disable entirely.
Timeout
Allows requests through if Redis does not respond within the specified duration. Prevents network issues from blocking legitimate traffic.
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
timeout: 1000, // ms; default is 5000
});Analytics
Enables request metrics visible in the Upstash Console (allowed, rate-limited, denied counts; top identifiers; geographic distribution).
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(60, "10 s"),
analytics: true,
});Multiple Limits (Tiered Rate Limiting)
Create separate Ratelimit instances with distinct prefixes to enforce per-tier rules.
const ratelimit = {
free: new Ratelimit({
redis,
prefix: "ratelimit:free",
limiter: Ratelimit.slidingWindow(10, "10 s"),
}),
paid: new Ratelimit({
redis,
prefix: "ratelimit:paid",
limiter: Ratelimit.slidingWindow(60, "10 s"),
}),
};Custom Token Consumption
The rate field in limit() subtracts a variable number of tokens per call, useful for batch APIs.
const { success } = await ratelimit.limit("user_123", { rate: 5 });Multi-Region
MultiRegionRatelimit replicates state across multiple Redis instances using CRDTs. Returns from the nearest replica immediately; synchronization is asynchronous.
import { MultiRegionRatelimit } from "@upstash/ratelimit";
const ratelimit = new MultiRegionRatelimit({
redis: [
new Redis({ url: "...", token: "..." }), // us-east-1
new Redis({ url: "...", token: "..." }), // eu-west-1
],
limiter: MultiRegionRatelimit.slidingWindow(10, "10 s"),
analytics: true,
});Dynamic Limits
Adjust the rate limit at runtime without recreating the instance.
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
dynamicLimits: true,
});
await ratelimit.setDynamicLimit({ limit: 5 });
const { dynamicLimit } = await ratelimit.getDynamicLimit();
await ratelimit.setDynamicLimit({ limit: false }); // revert to constructor defaultNotes
- Ephemeral cache is only effective when the serverless function instance is reused (warm starts); cold starts always hit Redis
- Multi-region setups cannot guarantee the configured limit is never exceeded by a small margin due to asynchronous CRDT synchronization
- Token Bucket algorithm is not supported in multi-region mode
analytics: trueadds +1 Redis command perlimit()call (see costs.md)- Dynamic limits add +1 Redis command per
limit()orgetRemaining()call
Related
- overview.md
- methods.md
- traffic-protection.md
- costs.md
Strapi Integration
@upstash/strapi-plugin-upstash-ratelimit — official Strapi plugin that applies @upstash/ratelimit to Strapi API routes.
Signature / Usage
npm install --save @upstash/strapi-plugin-upstash-ratelimitConfigure in /config/plugins.ts:
export default {
"upstash-ratelimit": {
enabled: true,
config: {
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
prefix: "@strapi",
analytics: false,
strategy: [
{
methods: ["GET", "POST"],
path: "*",
limiter: {
algorithm: "fixed-window",
tokens: 10,
window: "20 s",
},
},
],
},
},
};Options / Props
Top-level plugin config
| Name | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Toggle the plugin on/off |
url | string | — | UPSTASH_REDIS_REST_URL value |
token | string | — | UPSTASH_REDIS_REST_TOKEN value |
prefix | string | "@strapi" | Redis key prefix (@strapi:<method>:<route>:<identifier>) |
analytics | boolean | false | Enable Upstash Console metrics |
strategy | Strategy[] | — | Array of per-route rate limit rules |
Strategy object
| Name | Type | Description |
|---|---|---|
methods | `Array<"GET"\ | "POST"\ |
path | string | Route pattern; supports wildcards (*) and params (/api/restaurants/:id) |
identifierSource | string | "ip" or "header.KEY_NAME" |
debug | boolean | Logs remaining tokens and block status per request |
limiter | LimiterConfig | Algorithm configuration |
LimiterConfig
| Name | Type | Description |
|---|---|---|
algorithm | `"fixed-window" \ | "sliding-window" \ |
tokens | number | Maximum allowed tokens per window |
window | string | Duration string (e.g., "10 s", "1 m") |
refillRate | number | Token Bucket only: tokens added per window |
Notes
- The plugin uses
@upstash/ratelimitinternally; the same algorithm behavior and Redis cost model applies - Multiple strategy entries can coexist; rules are evaluated in array order
- Requires
.envto containUPSTASH_REDIS_REST_TOKENandUPSTASH_REDIS_REST_URL
Related
- overview.md
- algorithms.md
- features.md
Methods
Instance methods on a Ratelimit (or MultiRegionRatelimit) object.
Signature / Usage
limit()
Core method. Checks whether a request should proceed.
ratelimit.limit(
identifier: string,
req?: {
geo?: Geo;
rate?: number;
ip?: string;
userAgent?: string;
country?: string;
}
): Promise<RatelimitResponse>const { success, remaining, reset, pending } = await ratelimit.limit("user_123");
if (!success) return new Response("Too many requests", { status: 429 });
context.waitUntil(pending); // flush analytics / multi-region syncblockUntilReady()
Waits until the rate limit window resets before resolving, instead of immediately rejecting.
ratelimit.blockUntilReady(identifier: string, timeout: number): Promise<RatelimitResponse>const { success } = await ratelimit.blockUntilReady("user_123", 30_000);resetUsedTokens()
Clears all algorithm state for a given identifier.
ratelimit.resetUsedTokens(identifier: string): Promise<void>getRemaining()
Returns the current token count and next reset time without consuming a request.
ratelimit.getRemaining(identifier: string): Promise<{ remaining: number; reset: number }>setDynamicLimit()
Overrides the default limit at runtime. Pass false to revert to the constructor-defined limit.
ratelimit.setDynamicLimit(options: { limit: number | false }): Promise<void>getDynamicLimit()
Retrieves the currently active dynamic limit, or null if none is set.
ratelimit.getDynamicLimit(): Promise<{ dynamicLimit: number | null }>Options / Props
limit() — req parameter
| Name | Type | Description |
|---|---|---|
geo | Geo | Geolocation data object (passed through to deny-list checks) |
rate | number | Custom token consumption amount for this request (default: 1) |
ip | string | Client IP address (used by traffic protection) |
userAgent | string | Client user-agent string (used by traffic protection) |
country | string | Two-letter country code (used by traffic protection) |
RatelimitResponse
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the request is allowed |
limit | number | Configured maximum requests per window |
remaining | number | Remaining allowed requests in current window |
reset | number | Unix timestamp (ms) when the window resets |
pending | Promise<unknown> | Background work (analytics write, multi-region sync) |
reason | `"timeout" \ | "cacheBlock" \ |
deniedValue | string | The specific value matched in the deny list |
Notes
- Always await or pass
pendingtocontext.waitUntil()in serverless environments to avoid premature runtime shutdown blockUntilReady()accepts timeout in milliseconds; it retries on each window reset until success or timeoutsetDynamicLimit()/getDynamicLimit()requiredynamicLimits: truein the constructorgetRemaining()does not consume tokens; safe to call for informational headers
Related
- overview.md
- algorithms.md
- features.md
- costs.md
Overview
@upstash/ratelimit — connectionless (HTTP-based) rate limiting library for serverless and edge environments. Requires no persistent TCP connections and integrates with Upstash Redis.
Signature / Usage
npm install @upstash/ratelimit @upstash/redisimport { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
analytics: true,
prefix: "@upstash/ratelimit",
});
const { success } = await ratelimit.limit("api");
if (!success) return "Rate limited";For Deno:
import { Ratelimit } from "https://cdn.skypack.dev/@upstash/ratelimit@latest";Options / Props
| Name | Type | Default | Description |
|---|---|---|---|
redis | Redis | — | Upstash Redis instance (use Redis.fromEnv() or pass credentials explicitly) |
limiter | Algorithm | — | Rate limiting algorithm: fixedWindow, slidingWindow, or tokenBucket |
prefix | string | "@upstash/ratelimit" | Redis key namespace prefix to avoid collisions |
analytics | boolean | false | Enables request tracking in Upstash Console |
timeout | number | 5000 | Milliseconds to wait for Redis before allowing the request through |
ephemeralCache | `Map \ | false` | built-in |
enableProtection | boolean | false | Enables deny-list traffic protection (IP, user agent, country) |
dynamicLimits | boolean | false | Enables runtime limit adjustment via setDynamicLimit() |
Notes
- Designed for AWS Lambda, Vercel, Cloudflare Workers, Fastly, Next.js, and client-side apps
- Environment variables required:
UPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKEN - In serverless runtimes, await the
pendingpromise to ensure background tasks complete before shutdown:context.waitUntil(pending) MultiRegionRatelimitis the multi-region variant; its constructor accepts an array ofRedisinstances
Related
- algorithms.md
- methods.md
- features.md
ratelimit
| Name | Description | Path |
|---|---|---|
| Overview | Package overview, installation, and constructor options for @upstash/ratelimit | overview.md |
| Algorithms | Fixed Window, Sliding Window, and Token Bucket algorithm reference | algorithms.md |
| Methods | limit(), blockUntilReady(), resetUsedTokens(), getRemaining(), setDynamicLimit(), getDynamicLimit() | methods.md |
| Features | Ephemeral cache, timeout, analytics, multi-region, multiple limits, dynamic limits, custom rates | features.md |
| Traffic Protection | Deny-list blocking by IP, user agent, country; auto IP deny list | traffic-protection.md |
| Redis Command Costs | Commands consumed per algorithm and feature for billing estimation | costs.md |
| Strapi Integration | @upstash/strapi-plugin-upstash-ratelimit setup and configuration | integrations-strapi.md |
Traffic Protection
Deny-list based request blocking by IP address, user agent, country, or arbitrary identifier. Managed via the Upstash Ratelimit Dashboard.
Signature / Usage
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
enableProtection: true,
analytics: true,
});
const { success, reason, deniedValue } = await ratelimit.limit(identifier, {
ip: request.headers.get("x-forwarded-for") ?? "",
userAgent: request.headers.get("user-agent") ?? "",
country: request.geo?.country ?? "",
});
if (!success && reason === "denyList") {
console.log("Blocked:", deniedValue);
}Options / Props
| Name | Type | Description |
|---|---|---|
enableProtection | boolean | Enable deny-list checking on each limit() call |
ip | string (in req) | Client IP address to check against the deny list |
userAgent | string (in req) | User-agent string to check against the deny list |
country | string (in req) | Two-letter country code to check against the deny list |
RatelimitResponse fields (when blocked by deny list)
| Field | Value | Description |
|---|---|---|
success | false | Request is blocked |
reason | "denyList" | Block reason identifier |
deniedValue | string | The exact value matched in the deny list |
Notes
- Deny-list entries are managed in the Upstash Console / Ratelimit Dashboard; the library reads from Redis at runtime
- Blocked values are cached locally for ~1 minute, reducing Redis round-trips
- Exact matching only — wildcard or regex patterns are not supported
- Auto IP deny list: when enabled, malicious IPs are sourced from the ipsum aggregator (30+ deny lists), updated daily at 02:00 UTC via background refresh
- Adding
enableProtection: truecosts +2 extra Redis commands perlimit()call (see costs.md) analytics: trueis recommended alongsideenableProtectionto monitor denied requests in the dashboard
Related
- overview.md
- methods.md
- features.md
- costs.md
Generic Commands
Key management, expiry, scanning, and pub/sub commands that apply across all Redis data types.
Signature / Usage
// Key existence and deletion
await redis.exists("key1", "key2") // returns count of existing keys
await redis.del("key1", "key2") // delete keys
// TTL / Expiry
await redis.expire("key", 60) // seconds
await redis.pexpire("key", 1000) // milliseconds
await redis.expireat("key", unixSec) // Unix timestamp (seconds)
const ttl = await redis.ttl("key") // -1 = no TTL, -2 = key missing
const pttl = await redis.pttl("key") // milliseconds remaining
// Type and rename
const type = await redis.type("key") // "string" | "list" | "set" | etc.
await redis.rename("old", "new")
await redis.renamenx("old", "new") // rename only if new key doesn't exist
// SCAN — cursor-based iteration (avoids blocking KEYS)
const [cursor, keys] = await redis.scan(0, { match: "user:*", count: 100 })
// Pub/Sub
const listeners = await redis.publish("channel", "message")
// SUBSCRIBE is available via the REST API
// Lua scripting
const result = await redis.eval("return KEYS[1]", ["mykey"], [])
await redis.evalsha(sha1, ["mykey"], [])
// Server management
await redis.flushdb() // delete all keys in current DB
await redis.dbsize() // count of keys
await redis.ping() // health check
await redis.info() // server infoNotes
- Prefer
SCANoverKEYSin production —KEYSblocks the server while iterating DELreturns the count of keys actually deletedEXPIREreturns1if the timeout was set,0if the key does not exist- Lua scripts run atomically; access keys via
KEYSarray and args viaARGVarray SUBSCRIBE/PSUBSCRIBEkeep a connection open; use REST API/subscribeendpoint or a dedicated TCP client for long-lived subscriptions
Related
- Commands: String
- Pipelining & Transactions
- TypeScript SDK Overview
Hash Commands
Redis hash operations for storing and retrieving field-value maps within a single key.
Signature / Usage
// HSET — set one or more fields
await redis.hset("user:1", { name: "Alice", age: 30 })
// HGET — get a single field
const name = await redis.hget<string>("user:1", "name")
// HGETALL — get all fields and values
const user = await redis.hgetall<{ name: string; age: number }>("user:1")
// HMGET — get multiple fields
const [name, age] = await redis.hmget("user:1", "name", "age")
// HDEL — delete one or more fields
const removed = await redis.hdel("user:1", "field1", "field2")
// returns count of actually deleted fields
// HEXISTS — check if field exists
const exists = await redis.hexists("user:1", "name")
// HLEN — number of fields
const count = await redis.hlen("user:1")
// HKEYS / HVALS — list all field names or values
const keys = await redis.hkeys("user:1")
const vals = await redis.hvals("user:1")
// HINCRBY / HINCRBYFLOAT — increment numeric field
await redis.hincrby("user:1", "age", 1)
await redis.hincrbyfloat("user:1", "score", 1.5)Notes
HGETALLreturnsnullif the key does not existHMSETis deprecated in Redis 4.0; useHSETwith multiple fields insteadHDELreturns the count of fields that were actually deleted (ignores non-existent fields)
Related
- Commands: String
- Commands: JSON
- TypeScript SDK Overview
JSON Commands
Redis JSON commands for storing and manipulating JSON documents within Redis keys. Uses JSONPath ($) syntax for nested access.
Signature / Usage
// JSON.SET — store a JSON document at a path
await redis.json.set("user:1", "$", { name: "Alice", scores: [10, 20] })
await redis.json.set("user:1", "$.name", "Bob") // update nested field
// JSON.GET — retrieve JSON document or a nested value
const doc = await redis.json.get("user:1")
const name = await redis.json.get("user:1", { path: "$.name" })
// JSON.DEL — delete a path (or whole key if $ path)
await redis.json.del("user:1", "$.scores")
// JSON.ARRAPPEND — append to a JSON array at path
await redis.json.arrappend("user:1", "$.scores", 30, 40)
// returns array of new lengths
// JSON.ARRLEN — get array length at path
const len = await redis.json.arrlen("user:1", "$.scores")
// JSON.NUMINCRBY — increment numeric field
await redis.json.numincrby("user:1", "$.scores[0]", 5)
// JSON.TYPE — get type of value at path
const type = await redis.json.type("user:1", "$")
// JSON.MGET — get JSON values from multiple keys at same path
const vals = await redis.json.mget(["user:1", "user:2"], "$")Notes
- JSONPath root is
$; use dot notation for nested fields:$.address.city - Commands return arrays of values (one per matched path);
$returns one-element arrays - Works natively with Redis Search — use
json.setto index documents for full-text search JSON.SETcreates the key if it does not exist; useNXorXXoption to control behavior
Related
- Commands: String
- Redis Search: Getting Started
- TypeScript SDK Overview
List Commands
Redis list operations for ordered collections with head/tail push and pop.
Signature / Usage
// LPUSH / RPUSH — prepend or append elements
await redis.lpush("queue", "a", "b", "c") // ["c","b","a"]
await redis.rpush("queue", "x", "y") // ["c","b","a","x","y"]
// LPOP / RPOP — remove and return from head or tail
const head = await redis.lpop<string>("queue")
const tail = await redis.rpop<string>("queue")
// LRANGE — get a range of elements (0-indexed, inclusive)
const items = await redis.lrange<string>("queue", 0, -1) // all elements
// LLEN — number of elements
const len = await redis.llen("queue")
// LINDEX — get element by index
const el = await redis.lindex<string>("queue", 0)
// LINSERT — insert before or after a pivot element
await redis.linsert("queue", "BEFORE", "pivot", "newval")
// LSET — set element at index
await redis.lset("queue", 0, "newval")
// LREM — remove N occurrences of a value
await redis.lrem("queue", 1, "a")
// LTRIM — trim list to a range
await redis.ltrim("queue", 0, 9) // keep first 10 elements
// LMOVE — atomically move element between lists
await redis.lmove("src", "dst", "LEFT", "RIGHT")Notes
LRANGEwith-1as the end index returns up to the last elementLPUSH/RPUSHaccept multiple values; they are pushed in argument order- Blocking variants (
BLPOP,BRPOP) are supported for queue patterns - Lists are ordered by insertion — use Sorted Sets for priority ordering
Related
- Commands: String
- Commands: Sorted Set
- TypeScript SDK Overview
Set Commands
Redis set operations for unordered collections of unique members.
Signature / Usage
// SADD — add members; returns count of newly added
await redis.sadd("tags", "a", "b", "c") // 3
await redis.sadd("tags", "a", "b") // 0 (already exist)
// SREM — remove members
await redis.srem("tags", "a")
// SMEMBERS — get all members
const members = await redis.smembers("tags")
// SISMEMBER — check membership (boolean)
const exists = await redis.sismember("tags", "b")
// SMISMEMBER — check multiple memberships
const results = await redis.smismember("tags", "a", "b", "z")
// SCARD — count members
const count = await redis.scard("tags")
// SPOP — remove and return a random member
const member = await redis.spop<string>("tags")
// SRANDMEMBER — return random member(s) without removing
const rand = await redis.srandmember<string>("tags")
const rands = await redis.srandmember<string>("tags", 3)
// Set operations
const union = await redis.sunion("set1", "set2")
const inter = await redis.sinter("set1", "set2")
const diff = await redis.sdiff("set1", "set2")
// Store results of set operations into a new key
await redis.sunionstore("dest", "set1", "set2")
await redis.sinterstore("dest", "set1", "set2")
await redis.sdiffstore("dest", "set1", "set2")
// SMOVE — atomically move member between sets
await redis.smove("src", "dst", "member")Notes
- Sets do not preserve insertion order; use Sorted Sets for ordered unique collections
SRANDMEMBERwith a negative count may return duplicate members
Related
- Commands: Sorted Set
- Commands: Generic
- TypeScript SDK Overview
String Commands
Core Redis string operations for key-value storage, atomic counters, and bulk get/set.
Signature / Usage
// SET — store a value with optional expiration
await redis.set("key", "value")
await redis.set("key", "value", { ex: 60 }) // expire in 60 seconds
await redis.set("key", "value", { nx: true }) // only set if not exists
// GET — retrieve a value
const val = await redis.get<string>("key")
// MGET / MSET — bulk operations (single billing command)
const [a, b] = await redis.mget("key1", "key2")
await redis.mset({ key1: { a: 1 }, key2: "value2", key3: true })
// INCR / DECR — atomic counters
await redis.incr("counter")
await redis.incrby("counter", 5)
await redis.decr("counter")
await redis.decrby("counter", 3)
// APPEND — append to existing string
await redis.append("key", " world")
// STRLEN — get string length
const len = await redis.strlen("key")
// SETEX / SETNX / PSETEX
await redis.setex("key", 60, "value") // set with seconds TTL
await redis.setnx("key", "value") // set if not exists
await redis.psetex("key", 1000, "value") // set with milliseconds TTLOptions / Props
| Option (SET) | Type | Description |
|---|---|---|
ex | number | Expire in seconds |
px | number | Expire in milliseconds |
exat | number | Expire at Unix timestamp (seconds) |
pxat | number | Expire at Unix timestamp (milliseconds) |
nx | boolean | Only set if key does not exist |
xx | boolean | Only set if key already exists |
keepTtl | boolean | Retain the existing TTL |
get | boolean | Return the old value before setting |
Notes
MGETandMSETcount as a single command for billing regardless of number of keys- Values are automatically serialized/deserialized as JSON (disable with
automaticDeserialization: false) GETSETis deprecated in Redis 6.2; preferSET key value GEToptionSUBSTRis an alias forGETRANGE
Related
- Commands: Hash
- Commands: Key Expiry & Generic
- TypeScript SDK Overview
Sorted Set Commands
Redis sorted set operations for ordered collections where each member has a numeric score.
Signature / Usage
// ZADD — add members with scores
await redis.zadd("leaderboard",
{ score: 100, member: "alice" },
{ score: 200, member: "bob" },
)
// ZADD with options
await redis.zadd("leaderboard", { nx: true }, { score: 300, member: "carol" })
await redis.zadd("leaderboard", { incr: true }, { score: 10, member: "alice" })
// ZRANGE — get members by rank (0-indexed)
const top = await redis.zrange<string>("leaderboard", 0, -1)
// with scores
const withScores = await redis.zrange("leaderboard", 0, -1, { withScores: true })
// ZRANK / ZREVRANK — get rank (0 = lowest score)
const rank = await redis.zrank("leaderboard", "alice")
const revRank = await redis.zrevrank("leaderboard", "alice")
// ZSCORE — get score of a member
const score = await redis.zscore("leaderboard", "alice")
// ZREM — remove members
await redis.zrem("leaderboard", "alice", "bob")
// ZCARD — count members
const count = await redis.zcard("leaderboard")
// ZCOUNT — count members within score range
const inRange = await redis.zcount("leaderboard", 100, 200)
// ZINCRBY — increment score
await redis.zincrby("leaderboard", 50, "alice")
// ZPOPMIN / ZPOPMAX — remove and return lowest/highest scored members
const lowest = await redis.zpopmin<string>("leaderboard")
const highest = await redis.zpopmax<string>("leaderboard")
// ZRANGEBYSCORE — get members within score range
const range = await redis.zrangebyscore("leaderboard", 100, 200)Options / Props
| Option (ZADD) | Description |
|---|---|
nx | Only add new members; do not update existing |
xx | Only update existing members; do not add new |
ch | Return count of added + updated members (instead of only added) |
incr | Increment score by given value (single member only) |
gt | Update only if new score > current score |
lt | Update only if new score < current score |
Notes
- Members are sorted from lowest to highest score by default
ZRANGEwithREVflag (Redis 6.2+) returns members in reverse order- Use sorted sets for leaderboards, priority queues, and time-series indexing by timestamp
Related
- Commands: Set
- Commands: Generic
- TypeScript SDK Overview
Command Compatibility
Upstash Redis supports the Redis client protocol up to version 8.2. Over 200 commands are implemented across all major data types.
Notes
Supported Command Categories
- Strings: GET, SET, MGET, MSET, INCR, DECR, APPEND, STRLEN, SETEX, SETNX, GETRANGE, and more
- Lists: LPUSH, RPUSH, LPOP, RPOP, LRANGE, LLEN, LINDEX, BLPOP, BRPOP, and more
- Sets: SADD, SREM, SMEMBERS, SISMEMBER, SCARD, SUNION, SINTER, SDIFF, and more
- Sorted Sets: ZADD, ZRANGE, ZRANK, ZSCORE, ZREM, ZCARD, ZINCRBY, ZPOPMIN, ZPOPMAX, and more
- Hashes: HSET, HGET, HGETALL, HDEL, HMGET, HKEYS, HVALS, HINCRBY, and more
- JSON: JSON.SET, JSON.GET, JSON.DEL, JSON.ARRAPPEND, JSON.NUMINCRBY, and more
- Streams: XADD, XREAD, XRANGE, XLEN, and more
- HyperLogLog: PFADD, PFCOUNT, PFMERGE
- Geo: GEOADD, GEODIST, GEOPOS, GEOSEARCH
- Pub/Sub: PUBLISH, SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE
- Scripting: EVAL, EVALSHA, SCRIPT LOAD
- Transactions: MULTI, EXEC, WATCH, DISCARD
- Functions: FCALL, FUNCTION LOAD
- Server: ACL, FLUSHDB, DBSIZE, MONITOR, SCAN, INFO, PING
Connectivity
- Both native Redis TCP and HTTPS REST API are supported
- Integration-tested with major clients: node-redis, ioredis, Jedis, Lettuce, go-redis, redis-py
Unsupported Commands
- Most unsupported commands are on the Upstash roadmap
- Contact support@upstash.com for information on specific commands
Related
- REST API
- TypeScript SDK Overview
- Python SDK Overview
Connection & Authentication
How to obtain credentials and connect to an Upstash Redis database from various clients and environments.
Signature / Usage
// TypeScript — from environment variables
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
// TypeScript — explicit credentials
const redis = new Redis({
url: "https://<db>.upstash.io",
token: "<token>",
})# redis-cli (TLS required)
redis-cli --tls -a PASSWORD -h ENDPOINT -p PORT# Python — ioredis-compatible URL
from upstash_redis import Redis
redis = Redis.from_env()Options / Props
| Name | Description |
|---|---|
UPSTASH_REDIS_REST_URL | REST endpoint URL; found in Upstash Console → Database → Details tab |
UPSTASH_REDIS_REST_TOKEN | Full-access auth token |
| Read-Only Token | Restricted token; only allows read commands; available in Console |
Notes
- TLS is always enabled — all connections use TLS/SSL; it cannot be disabled
- Two token types are available per database: standard (full privileges) and read-only
- Credentials can also be passed as a query parameter:
?_token=TOKEN - For ioredis / redis-py / Jedis / Go: use
rediss://:<PASSWORD>@<ENDPOINT>:<PORT>format withssl=True - IP allowlisting is available but has limitations in serverless environments where IPs are dynamic
- Store credentials via environment variables or a secret management system — never hardcode them
- ACL users can restrict access to specific commands and key patterns; available on paid plans
Related
- TypeScript SDK Overview
- Python SDK Overview
- REST API
- Security
Deployment Environments
Platform-specific configuration for @upstash/redis across Node.js, Cloudflare Workers, Fastly, and Deno/edge runtimes.
Signature / Usage
// Node.js / Vercel / AWS Lambda / Netlify
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
// Node.js v17 and earlier — requires fetch polyfill
import "isomorphic-fetch"
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
// Cloudflare Workers (module syntax — pass env object)
export default {
fetch(request, env) {
const redis = Redis.fromEnv(env)
// ...
}
}
// Fastly Compute@Edge — requires explicit backend name
const redis = new Redis({
url: UPSTASH_REDIS_REST_URL,
token: UPSTASH_REDIS_REST_TOKEN,
backend: "upstash-backend", // defined in fastly.toml
})
// Deno / Netlify Edge
import { Redis } from "https://deno.land/x/upstash_redis/mod.ts"
const redis = Redis.fromEnv()Notes
- Node.js v18+: native
fetchis available; no polyfill needed - Node.js v17 and earlier: install
isomorphic-fetchand import it before using the SDK - Cloudflare Workers: use
wrangler secret putor the dashboard to setUPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKEN; for module workers, passenvtoRedis.fromEnv(env) - Fastly: configure a backend named entry in
fastly.tomlpointing to your Upstash endpoint - Initialize the Redis client outside the request handler to reuse across invocations (serverless warm start optimization)
Related
- TypeScript SDK Overview
- Connection & Authentication
Durability
Upstash Redis uses always-on persistence backed by cloud block storage (AWS EBS). Data is written to both memory and block storage on every write.
Notes
- Persistence is always enabled — there are no configurable persistence modes (no AOF/RDB user controls)
- Every write is stored in both memory and block storage simultaneously
- Data evicted from memory is retained in block storage — eviction does not cause data loss
- When a read targets an evicted key, it is automatically reloaded from block storage into memory
- Paid tier databases include multi-instance replication for additional fault tolerance
- Block storage is region-local; for multi-region durability, use Global Replication
Related
- Eviction
- Replication
- Global Replication
Eviction
Upstash Redis supports an optional eviction policy that automatically removes keys when the database reaches its size limit.
Notes
- Eviction is disabled by default — writes are rejected when the size limit is reached unless eviction is enabled
- Eviction can be enabled at database creation time or later via the database configuration panel
- Upstash implements a single eviction algorithm: Optimistic-Volatile
- Randomly samples keys for eviction, giving priority to keys with a TTL
- When volatile (TTL-bearing) keys are insufficient, non-volatile keys are randomly selected
- Combines aspects of
volatile-randomandallkeys-randomRedis policies - Additional eviction policies are planned for future releases
- Recommended for cache use cases where data is ephemeral or frequently regenerated
Related
- Durability
- Commands: Generic
Global Replication
Multi-region Upstash Redis that automatically replicates data across selected AWS regions. Read commands are routed to the nearest replica; writes go to the primary region.
Signature / Usage
// No SDK change required — global routing is transparent
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
// Reads are automatically served from the nearest replica
const value = await redis.get("key")
// Writes route to the primary and propagate to all read replicas
await redis.set("key", "value")Options / Props
| Setting | Description |
|---|---|
| Primary Region | AWS region that handles all write operations |
| Read Regions | Additional AWS regions that receive replicated data and serve reads |
Notes
- Supports 12+ AWS regions across North America, Europe, Asia-Pacific, and South America
- Read replicas can be added or removed with zero downtime
- Sub-millisecond read latency achievable when client and replica are in the same AWS region
- Write latency is determined by the primary region — choose the region closest to write-heavy workloads
- Global databases are required for SOC-2, HIPAA compliance features, and VPC Peering
- ACL users are not migrated automatically when moving from regional to global — redefine them after migration
- Migrating from regional to global: create a backup on the regional DB, restore to the global DB; this flushes the destination first
Related
- Replication
- Connection & Authentication
Pipelining & Transactions
Batch multiple Redis commands into a single HTTP request to reduce network roundtrips. Pipelining is non-atomic; transactions (multi()) are atomic.
Signature / Usage
// Pipeline — non-atomic batch
const p = redis.pipeline()
p.set("key", 2)
p.incr("key")
const [setResult, incrResult] = await p.exec<[string, number]>()
// Transaction — atomic batch
const tx = redis.multi()
tx.set("foo", "bar")
tx.get("foo")
const [setResult, getResult] = await tx.exec()# Python — pipeline
pipeline = redis.pipeline()
pipeline.set("foo", 1).incr("foo").get("foo")
result = pipeline.exec()
# Python — transaction
pipeline = redis.multi()
pipeline.set("foo", 1).get("foo")
result = pipeline.exec()Options / Props
| Method | Description |
|---|---|
redis.pipeline() | Creates a non-atomic pipeline; other client commands may interleave |
redis.multi() | Creates an atomic transaction; no other commands run between queued commands |
p.exec<[T1, T2]>() | Executes queued commands; returns typed array of results |
Notes
- Pipeline results are returned as an ordered array matching the command sequence
- Transactions guarantee atomicity — use when operations must be consistent with each other
- Auto-pipelining (TypeScript) can automatically batch commands sent in the same event loop tick
- The REST API equivalents are
/pipeline(non-atomic) and/multi-exec(atomic) - Pipelines count as a single HTTP request for billing purposes
Related
- REST API
- TypeScript SDK Overview
- Python SDK Overview
Python SDK — Pipelining & Transactions
Batch multiple Redis commands into a single HTTP request using the Python SDK. Pipelines are non-atomic; transactions are atomic.
Signature / Usage
from upstash_redis import Redis
redis = Redis.from_env()
# Pipeline — non-atomic
pipeline = redis.pipeline()
pipeline.set("foo", 1).incr("foo").get("foo")
result = pipeline.exec()
# result is a list: [True, 2, 2]
# Transaction (multi) — atomic
tx = redis.multi()
tx.set("foo", 1).get("foo")
result = tx.exec()
# Async pipeline
from upstash_redis.asyncio import Redis as AsyncRedis
redis = AsyncRedis.from_env()
pipeline = redis.pipeline()
pipeline.set("key", "val").get("key")
result = await pipeline.exec()Notes
- Method chaining is supported:
pipeline.set(...).incr(...).get(...) - Call
.exec()(orawait .exec()in async) to send all queued commands as a single HTTP request - Pipeline results are ordered lists; each element corresponds to a queued command's return value
- Transactions guarantee no other commands execute between queued commands
- Use
redis.execute(["COMMAND", "arg"])for any Redis command not yet implemented in the SDK
Related
- Python SDK Overview
- Pipelining & Transactions
- REST API
upstash-redis — Python SDK Overview
Connectionless, HTTP-based Redis client for Python, designed for serverless and serverful environments. Requires Python 3.8+. Supports both synchronous and asynchronous operation.
Signature / Usage
from upstash_redis import Redis
# Manual initialization
redis = Redis(url="UPSTASH_REDIS_REST_URL", token="UPSTASH_REDIS_REST_TOKEN")
# Auto-load from environment variables
redis = Redis.from_env()
# Basic usage
redis.set("key", "value")
value = redis.get("key")
# Async usage
from upstash_redis.asyncio import Redis as AsyncRedis
redis = AsyncRedis.from_env()
await redis.set("key", "value")
value = await redis.get("key")Options / Props
| Name | Type | Description |
|---|---|---|
url | str | REST endpoint URL from Upstash Console |
token | str | Auth token (standard or read-only) |
rest_encoding | `str \ | None` |
rest_retries | int | Number of automatic retries (default: 1) |
rest_retry_interval | float | Seconds between retries (default: 3) |
allow_telemetry | bool | Disable anonymous telemetry with False (default: True) |
Notes
- Install:
pip install upstash-redis Redis.from_env()readsUPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKEN- Async client is in
upstash_redis.asynciomodule - Initialize the client outside request handlers to maximize reuse across invocations
- Unimplemented commands can be run directly:
redis.execute(["XLEN", "stream_key"])
Related
- Connection & Authentication
- TypeScript SDK Overview
- Python SDK Pipelining & Transactions
Redis
| Name | Description | Path |
|---|---|---|
| TypeScript SDK Overview | HTTP/REST Redis client for TS; installation, initialization, Redis.fromEnv() | ts-sdk-overview.md |
| Python SDK Overview | HTTP/REST Redis client for Python; sync and async usage, Redis.from_env() | py-sdk-overview.md |
| Connection & Authentication | Credentials, environment variables, TLS, token types, multi-client connection strings | connection-auth.md |
| REST API | HTTP endpoint format, authentication, pipelining, transactions, response format | rest-api.md |
| Pipelining & Transactions | Batch commands with pipeline() (non-atomic) and multi() (atomic) | pipelining-transactions.md |
| Python SDK — Pipelining & Transactions | Python-specific pipelining and transaction patterns, method chaining | py-pipelining.md |
| Commands: String | GET, SET, MGET, MSET, INCR, DECR, APPEND, SETEX, SETNX and options | commands-string.md |
| Commands: Hash | HSET, HGET, HGETALL, HDEL, HMGET, HKEYS, HINCRBY and more | commands-hash.md |
| Commands: List | LPUSH, RPUSH, LPOP, RPOP, LRANGE, LLEN, LINDEX, LMOVE and more | commands-list.md |
| Commands: Set | SADD, SREM, SMEMBERS, SISMEMBER, SUNION, SINTER, SDIFF and more | commands-set.md |
| Commands: Sorted Set | ZADD, ZRANGE, ZRANK, ZSCORE, ZREM, ZINCRBY, ZPOPMIN/MAX and options | commands-zset.md |
| Commands: JSON | JSON.SET, JSON.GET, JSON.DEL, JSON.ARRAPPEND, JSON.NUMINCRBY (JSONPath $) | commands-json.md |
| Commands: Generic | Key expiry, SCAN, DEL, TYPE, Pub/Sub PUBLISH, Lua EVAL, FLUSHDB | commands-generic.md |
| Global Replication | Multi-region setup; read routing to nearest replica; zero-downtime region management | global-replication.md |
| Replication | Single-leader replication within a region; round-robin reads; Prod Pack multi-AZ | replication.md |
| Redis Search: Introduction | Full-text search built into Upstash Redis, powered by Tantivy; overview and concepts | search-introduction.md |
| Redis Search: Getting Started | Create index, index JSON documents, run queries and counts | search-getting-started.md |
| Redis Search: Query Operators | $must, $should, $mustNot, $boost, comparison operators ($lt, $gt, etc.) | search-query-operators.md |
| Redis Search: Aggregations | Metric ($avg, $sum, $count) and bucket ($terms, $histogram) aggregations | search-aggregations.md |
| Deployment Environments | Node.js, Cloudflare Workers, Fastly, Deno/edge — platform-specific setup | deployment.md |
| Security | TLS, ACL, IP allowlisting, encryption at rest, VPC Peering, PrivateLink | security.md |
| Durability | Always-on block storage persistence; memory + EBS dual-layer; no AOF/RDB modes | durability.md |
| Eviction | Optimistic-Volatile eviction policy; TTL-priority random key removal for cache use | eviction.md |
| Command Compatibility | 200+ supported commands; Redis protocol up to 8.2; unsupported items roadmap | compatibility.md |
Replication
Upstash Redis automatically replicates data across multiple instances within a region for high availability and read scalability. Available on all paid plans.
Signature / Usage
No configuration required in application code — replication is managed by Upstash.
// Reads distribute across replicas automatically
const redis = Redis.fromEnv()
const value = await redis.get("key")Notes
- Uses single-leader replication: each key has one leader replica that handles writes; backup replicas receive propagated updates
- Read requests are distributed across replicas using round-robin
- Primary replicas handle reads and writes; read replicas handle reads only
- If a replica becomes unavailable, other replicas continue serving traffic
- During a leader failure, a brief election may temporarily block requests until a new leader is selected
- Prod Pack (add-on) extends replication across multiple availability zones within a region for zone-level fault tolerance
- Additional replicas can be provisioned to scale read throughput without downtime
Related
- Global Replication
- Durability
REST API
HTTP-based interface for Upstash Redis. Translates Redis commands into REST endpoints. Useful for edge runtimes, browsers, and any environment that supports HTTP but not TCP.
Signature / Usage
# GET request — command/arg1/arg2/...
curl https://<db>.upstash.io/set/foo/bar \
-H "Authorization: Bearer $TOKEN"
# GET request — read value
curl https://<db>.upstash.io/get/foo \
-H "Authorization: Bearer $TOKEN"
# POST request — for JSON or binary values
curl -X POST https://<db>.upstash.io/set/key \
-H "Authorization: Bearer $TOKEN" \
-d '{"field":"value"}'
# Pipeline — multiple commands in one request
curl -X POST https://<db>.upstash.io/pipeline \
-H "Authorization: Bearer $TOKEN" \
-d '[["SET","key","val"],["GET","key"]]'
# Transaction — atomic multi-exec
curl -X POST https://<db>.upstash.io/multi-exec \
-H "Authorization: Bearer $TOKEN" \
-d '[["SET","key","val"],["GET","key"]]'Options / Props
| Header / Param | Description |
|---|---|
Authorization: Bearer $TOKEN | Standard auth header |
?_token=TOKEN | Token as query parameter (alternative to header) |
Upstash-Encoding: base64 | Receive response values as base64 |
Upstash-Response-Format: resp2 | Receive response in RESP2 format instead of JSON |
Notes
- Endpoint pattern:
REST_URL/COMMAND/arg1/arg2/... - Successful responses return
{ "result": ... }; failures return{ "error": "..." } - HTTP status codes:
200success,400bad request / syntax error,401unauthorized,405method not allowed /pipelinesends multiple commands in one HTTP request; execution is not atomic/multi-execexecutes commands atomically (equivalent toMULTI/EXEC)/monitorendpoint enables real-time command tracking- Pub/Sub is supported via
SUBSCRIBEandPUBLISHcommands over the REST API
Related
- Connection & Authentication
- Pipelining & Transactions
- TypeScript SDK Overview
Redis Search — Aggregations
Compute analytics metrics and bucket groupings over indexed documents. Aggregations run in two phases: optional filtering, then computation.
Signature / Usage
const result = await index.aggregate({
filter: { inStock: true },
aggregations: {
avg_price: { $avg: { field: "price" } },
total: { $count: {} },
by_category: { $terms: { field: "category", size: 5 } },
price_dist: { $histogram: { field: "price", interval: 50 } },
},
})
// Access results
console.log(result.avg_price.value) // number
console.log(result.by_category.buckets) // [{ key, docCount }]Options / Props
Metric Aggregations (numeric summaries)
| Function | Description |
|---|---|
$avg | Average of a numeric field |
$sum | Sum of a numeric field |
$min | Minimum value |
$max | Maximum value |
$count | Total document count |
$cardinality | Count of unique values |
$stats | Basic statistics (min, max, avg, sum, count) |
$extendedStats | Extended statistics including variance and std deviation |
$percentiles | Percentile distribution |
Bucket Aggregations (document grouping)
| Function | Description |
|---|---|
$terms | Group by top values of a field; size controls max buckets |
$range | Group by custom numeric ranges |
$histogram | Group by fixed-width numeric intervals |
$dateHistogram | Group by fixed time intervals |
$facet | Hierarchical facet aggregation |
Notes
- Multiple aggregations can be computed in a single request against the same filtered dataset
- Bucket aggregations support nested
$aggsfor per-bucket sub-metrics - Metric aggregations return
{ value: number }; bucket aggregations return{ buckets: [{ key, docCount, ... }] } - Results are keyed by the alias name provided in the
aggregationsobject
Related
- Redis Search: Query Operators
- Redis Search: Getting Started
Redis Search — Getting Started
Step-by-step guide to creating a search index, indexing documents, and running queries with Upstash Redis Search.
Signature / Usage
import { Redis } from "@upstash/redis"
import { s } from "@upstash/redis/search"
const redis = Redis.fromEnv()
// Step 1: Create an index (once per schema)
const index = await redis.search.createIndex({
name: "products",
dataType: "json", // "json" | "hash"
prefix: "product:", // keys with this prefix are auto-indexed
schema: s.object({
name: s.string(),
description: s.string(),
category: s.string().noTokenize(),
price: s.number(),
inStock: s.boolean(),
}),
})
// Step 2: Store documents matching the prefix
await redis.json.set("product:1", "$", {
name: "Wireless Headphones",
description: "Premium noise-cancelling wireless headphones",
category: "electronics",
price: 199.99,
inStock: true,
})
// Step 3: (Optional) Wait for indexing to complete
await index.waitIndexing()
// Step 4: Query
const results = await index.query({
filter: { description: "wireless" },
limit: 10,
})
// Step 5: Count matching documents
const count = await index.count({
filter: { price: { $lt: 150 } },
})Options / Props
createIndex option | Type | Description |
|---|---|---|
name | string | Unique index name |
dataType | `"json" \ | "hash"` |
prefix | string | Key prefix; only matching keys are indexed |
schema | s.object({...}) | Schema definition for indexed fields |
| Schema field type | Description |
|---|---|
s.string() | Full-text searchable string |
s.string().noTokenize() | Exact-match keyword (no tokenization) |
s.number() | Numeric field (supports range filters) |
s.boolean() | Boolean field |
Notes
createIndexshould be called once — repeated calls throw an error if the index already exists- Indexing is asynchronous; call
waitIndexing()in tests to ensure documents are searchable before querying - Documents are automatically de-indexed when the key is deleted
Related
- Redis Search: Introduction
- Redis Search: Query Operators
- Redis Search: Aggregations
Redis Search — Introduction
Full-text search built into Upstash Redis, powered by Tantivy (Rust-based). Enables searching through Redis data without a separate search service.
Signature / Usage
import { Redis } from "@upstash/redis"
import { s } from "@upstash/redis/search"
const redis = Redis.fromEnv()
// Create a search index (once, not per request)
const index = await redis.search.createIndex({
name: "products",
dataType: "json",
prefix: "product:",
schema: s.object({
name: s.string(),
description: s.string(),
category: s.string().noTokenize(), // exact-match only
price: s.number(),
inStock: s.boolean(),
}),
})
// Index a document (automatically tracked by key prefix)
await redis.json.set("product:1", "$", {
name: "Wireless Headphones",
description: "Premium noise-cancelling wireless headphones",
category: "electronics",
price: 199.99,
inStock: true,
})
// Wait for async indexing (useful in tests)
await index.waitIndexing()
// Query
const results = await index.query({
filter: { description: "wireless" },
})Notes
- Indexing is automatic and asynchronous — once an index is created, all matching write operations are tracked
- Create index once (typically at app startup or migration); re-creating throws an error
- Supports JSON, Hash, and String data types
- The Tantivy engine provides boolean operators, fuzzy matching, phrase queries, and regex support
- This is Upstash's first extension beyond the standard Redis spec
Related
- Redis Search: Getting Started
- Redis Search: Query Operators
- Redis Search: Aggregations
- Commands: JSON
Redis Search — Query Operators
Boolean and comparison operators for filtering documents in Upstash Redis Search queries.
Signature / Usage
// Simple filter — match a field value
const results = await index.query({
filter: { category: "electronics", inStock: true },
})
// Comparison operators
const cheap = await index.query({
filter: { price: { $lt: 50 } },
})
// Boolean: $must — all conditions required
const results = await index.query({
filter: {
$must: { category: "electronics", description: "wireless" },
},
})
// Boolean: $should — at least one condition (OR logic)
const results = await index.query({
filter: {
$should: [
{ $must: { category: "electronics", description: "premium" } },
{ $must: { category: "sports", price: { $lt: 50 } } },
],
},
})
// Boolean: $mustNot — exclude matching documents
const results = await index.query({
filter: {
$must: { inStock: true },
$mustNot: { category: "clearance" },
},
})
// $boost — adjust relevance scoring weight
const results = await index.query({
filter: {
$must: { description: "wireless" },
$should: { $boost: 2.0, description: "premium" },
},
})Options / Props
| Operator | Description |
|---|---|
$must | All specified conditions must match (AND) |
$should | Optional conditions; boosts score when combined with $must; acts as OR when alone |
$mustNot | Excludes documents matching any of these conditions |
$boost | Multiplies the relevance score contribution of a clause |
$lt | Numeric less-than comparison |
$lte | Numeric less-than-or-equal |
$gt | Numeric greater-than comparison |
$gte | Numeric greater-than-or-equal |
$eq | Exact equality |
$ne | Not equal |
Notes
- When
$shouldis used alongside$must, it acts as a score booster rather than a mandatory condition - Nested
$shouldarrays create OR logic between clause groups - Boolean operators can be nested for complex query expressions
- String fields support fuzzy matching, phrase queries, and regex in the filter value
Related
- Redis Search: Introduction
- Redis Search: Getting Started
- Redis Search: Aggregations
Security
Upstash Redis security features: TLS encryption, ACL, IP allowlisting, encryption at rest, and private connectivity options.
Notes
TLS
- TLS is always enabled on all Upstash Redis databases; it cannot be disabled
- All client connections (REST API and redis-cli) use TLS by default
Authentication Tokens
- Two token types per database: standard (full read/write) and read-only
- Tokens are shown in the Upstash Console under the database Details tab
- Store tokens in environment variables or a secrets manager — never hardcode them
Redis ACL
- Restricts specific users to a subset of commands and key patterns
- Available on all paid plans
- Works with the REST API through dedicated ACL tokens
- ACL users must be manually redefined when migrating from a regional to a global database
IP Allowlisting
- Limits database access to specified IP addresses
- Practical limitation: serverless functions use dynamic IPs, making allowlisting difficult in those environments
Encryption at Rest
- Available via the Prod Pack add-on
- Encrypts data stored in block storage (EBS)
Private Connectivity
- VPC Peering and AWS PrivateLink: Pro-tier features that bypass public internet for database access
Related
- Connection & Authentication
- Global Replication
@upstash/redis — TypeScript SDK Overview
HTTP/REST-based Redis client for TypeScript, built on the Upstash REST API. Connectionless by design — ideal for serverless, edge, and WebAssembly environments where TCP is unavailable.
Signature / Usage
import { Redis } from "@upstash/redis"
// Manual initialization
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
// Auto-load from environment variables
const redis = Redis.fromEnv()Options / Props
| Name | Type | Description |
|---|---|---|
url | string | REST endpoint URL from Upstash Console |
token | string | Auth token (standard or read-only) |
automaticDeserialization | boolean | Disable automatic JSON deserialization (default: true) |
responseEncoding | false | Disable base64 response encoding if values appear hashed |
enableTelemetry | boolean | Anonymous telemetry collection (default: true); also controllable via UPSTASH_DISABLE_TELEMETRY env var |
Notes
- Install:
npm install @upstash/redis(also supports yarn, pnpm, and Deno) Redis.fromEnv()readsUPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKENautomatically- On Node.js v17 and earlier,
fetchis not natively available — importisomorphic-fetchas a polyfill - Large numbers (above
2^53 - 1) are returned as strings due to JavaScript's number limit - Initialize the client outside request handlers in serverless environments to maximize reuse
Related
- Connection & Authentication
- Pipelining & Transactions
- Deployment Environments
- Python SDK Overview
delete
Removes vectors from an index by ID, ID prefix, or metadata filter.
Signature / Usage
// Delete by ID array
const result = await index.delete(["id-1", "id-2"]);
// { deleted: 2 }
// Delete single ID
const result = await index.delete("id-1");
// { deleted: 1 }
// Delete by ID prefix
const result = await index.delete({ prefix: "article_" });
// { deleted: 3 }
// Delete by metadata filter
const result = await index.delete({ filter: "age > 30" });
// { deleted: 3 }
// With namespace
const result = await index.delete(["id-1"], { namespace: "my-namespace" });Options / Props
First argument:
| Name | Type | Description |
|---|---|---|
ids | `string \ | number \ |
prefix | string | Delete all vectors whose ID starts with this prefix |
filter | string | Delete vectors matching this metadata filter expression |
Note: ids, prefix, and filter are mutually exclusive within a DeletePayload object.
Options (second argument):
| Name | Type | Description |
|---|---|---|
namespace | string | Target namespace. Omit to use the default namespace |
Response:
| Name | Type | Description |
|---|---|---|
deleted | number | Number of vectors successfully deleted |
Notes
- Filter-based deletion performs a full index scan (O(N)) — can be slow on large indexes
- Deleting a non-existent ID does not error;
deletedcount reflects only actual removals
Related
- upsert.md
- fetch.md
- filtering.md
- namespace.md
Embedding Models
Upstash Vector can automatically generate embeddings from raw text using a hosted model selected at index creation time. This eliminates the need to pre-vectorize data before upsert and query.
Signature / Usage
// With a hosted model, upsert text directly
await index.upsert({
id: "1",
data: "The Lord of the Rings follows Frodo Baggins...",
metadata: { title: "Lord of The Rings" },
});
// Query with text (embedding generated automatically)
const results = await index.query({
data: "hobbit adventure",
topK: 5,
includeMetadata: true,
includeData: true,
});Options / Props
Dense index models:
| Model | Dimensions | Seq Length | MTEB Score |
|---|---|---|---|
BAAI/bge-large-en-v1.5 | 1024 | 512 | 64.23 |
BAAI/bge-base-en-v1.5 | 768 | 512 | 63.55 |
BAAI/bge-small-en-v1.5 | 384 | 512 | 62.17 |
BAAI/bge-m3 | 1024 | 8192 | — (multilingual, extended context) |
Sparse / Hybrid index models:
| Model | Description |
|---|---|
BAAI/bge-m3 | Multi-functional, multi-lingual sparse embedder |
BM25 | Classic keyword ranking for information retrieval |
Notes
- Model is selected once at index creation and cannot be changed afterward
datafield is used in upsert/query when a hosted model is configured;vectorfield is used otherwiseincludeData: truein query/fetch returns the original text stored with each vectorBAAI/bge-m3supports up to 8,192-token context — suitable for long documents
Related
- ts-sdk-overview.md
- upsert.md
- query.md
- hybrid-indexes.md
- sparse-indexes.md
fetch
Retrieves vectors by their IDs. Returns null for IDs that do not exist.
Signature / Usage
// Simple ID array
const results = await index.fetch(["id-1", "id-2", "id-3"], {
includeMetadata: true,
includeVectors: true,
includeData: true,
});
// FetchPayload with prefix
const results = await index.fetch(
{ prefix: "article_" },
{ includeMetadata: true }
);
// Namespace-scoped fetch
const results = await index.namespace("my-namespace").fetch(["id-1"]);Options / Props
First argument (ids or FetchPayload):
| Name | Type | Description |
|---|---|---|
ids | `(string \ | number)[]` |
prefix | string | Retrieve all vectors whose ID starts with this prefix. Use range for large prefix scans |
Options (second argument):
| Name | Type | Description |
|---|---|---|
includeMetadata | boolean | Include metadata in response |
includeVectors | boolean | Include vector arrays in response |
includeData | boolean | Include the data field in response |
namespace | string | Target namespace. Omit to use the default namespace |
Response item fields:
| Name | Type | Description |
|---|---|---|
id | `string \ | number` |
vector | `number[] \ | null` |
sparseVector | object \ | null |
metadata | object \ | null |
data | `string \ | null` |
Notes
- Returns an array ordered to match the input
ids; missing IDs producenullat that position - For large prefix-based scans, prefer
rangewith aprefixfilter (stateless pagination)
Related
- upsert.md
- delete.md
- range.md
Metadata Filtering
SQL-like filter expressions that restrict vector similarity search results. Only vectors whose metadata matches the filter are returned.
Signature / Usage
// Equality
await index.query({ vector: [...], topK: 5, filter: "genre = 'fantasy'" });
// Numeric comparison
await index.query({ vector: [...], topK: 5, filter: "year >= 2020" });
// Boolean logic
await index.query({
vector: [...],
topK: 5,
filter: "genre = 'fantasy' AND year > 2000",
});
// IN operator
await index.query({ vector: [...], topK: 5, filter: "genre IN ('fantasy', 'sci-fi')" });
// Nested object (dot notation)
await index.query({ vector: [...], topK: 5, filter: "geography.country = 'US'" });
// Array membership
await index.query({ vector: [...], topK: 5, filter: "CONTAINS(tags, 'new')" });
// Pattern matching
await index.query({ vector: [...], topK: 5, filter: "title GLOB 'Lord*'" });Options / Props
Supported value types:
| Type | Example |
|---|---|
| String | 'fantasy', "sci-fi" |
| Number | 2020, 3.14 |
| Boolean | 1 (true), 0 (false) |
| Object | Accessed via dot notation |
| Array | Accessed via index or CONTAINS |
Operators:
| Operator | Description |
|---|---|
= / != | Equals / Not equals (string, number, boolean) |
< <= > >= | Numeric comparisons |
GLOB / NOT GLOB | Case-sensitive UNIX wildcard pattern (*, ?, []) |
IN (...) / NOT IN (...) | Match against a set of literals |
CONTAINS / NOT CONTAINS | Check array field for membership |
HAS FIELD / HAS NOT FIELD | Test for JSON key existence |
AND / OR | Boolean combination. AND has higher precedence than OR |
(...) | Grouping to override default precedence |
Advanced access patterns:
| Syntax | Description |
|---|---|
obj.field | Dot notation for nested objects |
arr[0] | Array index access |
arr[#-1] | Last element access |
Notes
- Filters use in-filtering combined with post-filtering. A per-query budget limits the number of candidate vectors evaluated; exhausting it triggers post-filtering, which may return fewer than
topKresults - String literals accept both single and double quotes
- Boolean values use
1/0, nottrue/false - Filter expressions are also supported in
delete(full scan, O(N))
Related
- query.md
- delete.md
Hybrid Indexes
Combines dense (semantic) and sparse (keyword) vector search. Both components are queried and their scores are fused, yielding better recall for out-of-domain queries where pure semantic search fails.
Signature / Usage
// Upsert with both dense and sparse vectors
await index.upsert({
id: "1",
vector: [0.1, 0.2, 0.3], // dense component
sparseVector: {
indices: [10, 45, 200],
values: [0.8, 0.3, 0.5],
},
metadata: { title: "Example doc" },
});
// Query hybrid index — fused results (default)
const results = await index.query({
vector: [0.1, 0.2, 0.3],
sparseVector: { indices: [10, 45], values: [0.8, 0.3] },
topK: 5,
fusionAlgorithm: "RRF", // default
queryMode: "HYBRID", // default
includeMetadata: true,
});
// Text-based query (when using a hosted embedding model)
const results = await index.query({
data: "search query text",
topK: 5,
queryMode: "DENSE", // DENSE | SPARSE | HYBRID
});Options / Props
`queryMode` values:
| Value | Description |
|---|---|
HYBRID | Query both dense and sparse components and fuse results (default) |
DENSE | Query only the dense component |
SPARSE | Query only the sparse component |
`fusionAlgorithm` values:
| Value | Description |
|---|---|
RRF | Reciprocal Rank Fusion — score = 1 / (rank + 60). Default. Good general-purpose fusion |
DBSF | Distribution-Based Score Fusion — normalizes scores using mean and standard deviation. More sensitive to score range differences |
Upsert sparse vector fields:
| Name | Type | Description |
|---|---|---|
indices | number[] | Dimension indices with non-zero values |
values | number[] | Values corresponding to each index (same length as indices) |
Notes
- Hybrid indexes require both dense and sparse vectors on upsert — neither component can be omitted
- When using hosted models (e.g., BGE-M3, BM25), upsert and query with
datastring; embedding is automatic - For specialized reranking needs, query dense and sparse components separately using
queryMode: 'DENSE'/queryMode: 'SPARSE'and apply a custom reranker (e.g., bge-reranker-v2-m3) - Sparse vectors have a maximum of 1,000 non-zero dimensions
Related
- sparse-indexes.md
- query.md
- embedding-models.md
info / reset
info retrieves index statistics. reset clears all vectors from a namespace or the entire index.
Signature / Usage
// Index statistics
const stats = await index.info();
// {
// vectorCount: 17,
// pendingVectorCount: 0,
// indexSize: 4096,
// dimension: 1536,
// similarityFunction: "COSINE",
// namespaces: {
// "": { vectorCount: 10, pendingVectorCount: 0 },
// "my-namespace": { vectorCount: 7, pendingVectorCount: 0 },
// }
// }
// Reset default namespace
await index.reset();
// Reset a specific namespace
await index.reset({ namespace: "my-namespace" });
// Reset all namespaces
await index.reset({ all: true });Options / Props
`info()` response fields:
| Field | Type | Description |
|---|---|---|
vectorCount | number | Total vectors ready for querying |
pendingVectorCount | number | Vectors still being indexed |
indexSize | number | Index size in bytes |
dimension | number | Vector dimensionality |
similarityFunction | string | Distance metric (COSINE, EUCLIDEAN, DOT_PRODUCT) |
namespaces | Record<string, { vectorCount, pendingVectorCount }> | Per-namespace statistics |
`reset(options)` parameters:
| Name | Type | Description |
|---|---|---|
namespace | string | Clear a specific namespace. Pass "" for default namespace |
all | true | Clear all namespaces simultaneously |
`reset()` response: Returns 'Successful' string.
Notes
pendingVectorCountreflects vectors that have been upserted but are not yet available for querying (eventual consistency)resetis irreversible — all vectors and metadata in the targeted scope are permanently deleted- Pass no arguments to
reset()to clear the default namespace only
Related
- namespace.md
- ts-sdk-overview.md
Namespace
Isolated partitions of a vector index. Each namespace acts as an independent subset; all read/write operations target a single namespace. Every index has a default namespace ("").
Signature / Usage
import { Index } from "@upstash/vector";
const index = new Index();
// Access a named namespace
const ns = index.namespace("my-namespace");
await ns.upsert({ id: "1", vector: [0.1, 0.2, 0.3] });
await ns.query({ vector: [0.1, 0.2, 0.3], topK: 5 });
await ns.fetch(["1"], { includeMetadata: true });
await ns.delete(["1"]);
await ns.range({ cursor: 0, limit: 100 });
// List all namespaces
const namespaces = await index.listNamespaces();
// ["", "my-namespace", "other-namespace"]
// Delete a namespace
await index.deleteNamespace("my-namespace");
// Reset a specific namespace (clear all vectors)
await index.reset({ namespace: "my-namespace" });
// Reset all namespaces
await index.reset({ all: true });Options / Props
`index.namespace(name)`:
| Name | Type | Description |
|---|---|---|
name | string | Namespace name. Use "" for the default namespace |
Returns a namespace-scoped client exposing the same methods as Index: upsert, query, fetch, delete, range, reset, resumableQuery.
`index.deleteNamespace(name)`:
| Name | Type | Description |
|---|---|---|
name | string | Name of the namespace to delete |
`index.listNamespaces()`: Returns string[] of all active namespace names.
`index.reset(options)`:
| Name | Type | Description |
|---|---|---|
namespace | string | Clear a single namespace (pass "" for default) |
all | true | Clear all namespaces simultaneously |
Notes
- Namespaces are created implicitly on first upsert — no explicit create step
- Deleting a namespace permanently removes all its vectors
- Pre-namespace indexes (created before the feature existed) continue to work against the default namespace automatically
- In Python SDK: pass
namespace="ns"as a keyword argument directly to each method (e.g.,index.query(..., namespace="ns"))
Related
- upsert.md
- query.md
- ts-sdk-overview.md
- python-sdk.md
Python SDK (upstash-vector)
HTTP-based Upstash Vector client for Python 3.8+. API mirrors the TypeScript SDK with Python naming conventions (snake_case, keyword arguments for options).
Signature / Usage
from upstash_vector import Index
# From environment variables
index = Index.from_env()
# Explicit credentials
index = Index(
url="UPSTASH_VECTOR_REST_URL",
token="UPSTASH_VECTOR_REST_TOKEN",
)
# Upsert
index.upsert(vectors=[
{"id": "1", "vector": [0.1, 0.2, 0.3], "metadata": {"genre": "fantasy"}},
])
# Query
results = index.query(
vector=[0.1, 0.2, 0.3],
top_k=5,
include_metadata=True,
include_data=True,
filter="genre = 'fantasy'",
)
# Fetch
fetch_result = index.fetch(
ids=["id-1", "id-2"],
include_vectors=True,
include_metadata=True,
include_data=True,
)
# Namespace (keyword argument on each call)
index.upsert(vectors=[...], namespace="my-namespace")
index.query(vector=[...], top_k=5, namespace="my-namespace")Options / Props
Differences from TypeScript SDK:
| Feature | TypeScript | Python |
|---|---|---|
| Install | npm install @upstash/vector | pip install upstash-vector |
| Init from env | new Index() | Index.from_env() |
| Parameter style | camelCase options object | snake_case keyword args |
| Namespace access | index.namespace("ns").query(...) | index.query(..., namespace="ns") |
| Batch query | index.query([...]) (array) | index.query_many([...]) |
topK param | topK | top_k |
includeMetadata | includeMetadata | include_metadata |
includeVectors | includeVectors | include_vectors |
includeData | includeData | include_data |
`Index` constructor options:
| Name | Type | Description |
|---|---|---|
url | str | REST URL from Upstash console |
token | str | REST token from Upstash console |
retries | int | Retry attempts on failure. Default: 3 |
retry_interval | float | Seconds between retries. Default: 1.0 |
allow_telemetry | bool | Anonymous usage telemetry. Default: True |
Notes
query_many()executes multiple queries in a single request (batch)- Built-in retry: up to 3 attempts, 1 second apart by default
- Initialize
Indexoutside request handlers in serverless environments to reuse connections - Telemetry collects SDK version, platform (Vercel, AWS), and Python runtime version only
Related
- ts-sdk-overview.md
- upsert.md
- query.md
- namespace.md
query
Retrieves the most similar vectors from an index using Approximate Nearest Neighbor (ANN) search. Scores are normalized between 0 and 1, where 1 is highest similarity.
Signature / Usage
// Dense vector query
const results = await index.query({
vector: [0.1, 0.2, 0.3],
topK: 5,
includeMetadata: true,
includeVectors: false,
filter: "genre = 'fantasy'",
});
// Text query (hosted embedding model required)
const results = await index.query({
data: "The Lord of the Rings",
topK: 5,
includeMetadata: true,
});
// Namespace-scoped query
const results = await index.namespace("my-namespace").query({
vector: [0.1, 0.2, 0.3],
topK: 5,
});Options / Props
Payload:
| Name | Type | Required | Description |
|---|---|---|---|
vector | number[] | Yes* | Dense query vector. Dimension must match index |
sparseVector | { indices: number[], values: number[] } | Yes* | Sparse query vector for sparse/hybrid indexes |
data | string | Yes* | Text to embed and query; requires hosted embedding model |
topK | number | Yes | Number of top results to return |
includeMetadata | boolean | No | Include metadata in results |
includeVectors | boolean | No | Include vector arrays in results |
includeData | boolean | No | Include the data field in results |
filter | string | No | Metadata filter expression (SQL-like syntax) |
fusionAlgorithm | `'RRF' \ | 'DBSF'` | No |
weightingStrategy | string | No | Weighting strategy for sparse vector dimensions |
queryMode | `'HYBRID' \ | 'DENSE' \ | 'SPARSE'` |
*Provide vector, sparseVector, or data depending on index type.
Options:
| Name | Type | Description |
|---|---|---|
namespace | string | Target namespace. Omit to use the default namespace |
Response item fields:
| Name | Type | Description |
|---|---|---|
id | `string \ | number` |
score | number | Similarity score (0–1) |
vector | number[] | Dense vector (if includeVectors: true) |
sparseVector | object | Sparse vector (if requested) |
metadata | object | Attached metadata (if includeMetadata: true) |
data | string | Stored data string (if includeData: true) |
Notes
- Filter budget: if exhausted during in-filtering, the system switches to post-filtering and may return fewer than
topKresults - For hybrid indexes,
queryModecontrols which components are searched fusionAlgorithm: 'DBSF'(Distribution-Based Score Fusion) is more sensitive to score range differences than the defaultRRF
Related
- filtering.md
- resumable-query.md
- hybrid-indexes.md
- upsert.md
range
Retrieves vectors in paginated chunks using a cursor. Stateless — all parameters must be passed in every request.
Signature / Usage
// First page
const page1 = await index.range({
cursor: 0,
limit: 100,
includeMetadata: true,
});
// Next page
const page2 = await index.range({
cursor: page1.nextCursor,
limit: 100,
includeMetadata: true,
});
// With ID prefix filter
const results = await index.range({
cursor: 0,
limit: 50,
prefix: "article_",
includeMetadata: true,
});
// Namespace-scoped
const results = await index.namespace("my-namespace").range({
cursor: 0,
limit: 100,
});Options / Props
Payload:
| Name | Type | Required | Description |
|---|---|---|---|
cursor | `string \ | number` | Yes |
limit | number | Yes | Maximum number of vectors to return per page |
prefix | string | No | Filter vectors by ID prefix |
includeMetadata | boolean | No | Include metadata in results |
includeVectors | boolean | No | Include vector arrays in results |
includeData | boolean | No | Include the data field in results |
namespace | string | No | Target namespace. Omit to use the default namespace |
Response:
| Name | Type | Description |
|---|---|---|
nextCursor | string | Cursor value to use in the next request. Empty string ("") indicates the last page |
vectors | array | Array of vector objects with id, and optionally vector, metadata, data |
Notes
- Stateless: repeat all parameters (including
cursor) on every request - Iteration ends when
nextCursoris an empty string - Prefer
rangeoverfetchwithprefixfor large datasets
Related
- fetch.md
- query.md
- namespace.md
vector
| Name | Description | Path |
|---|---|---|
| @upstash/vector TypeScript SDK | Installation, Index initialization, and type-safe client setup | ts-sdk-overview.md |
| upsert | Add or update vectors by ID, with optional metadata and namespace targeting | upsert.md |
| query | ANN similarity search with filtering, metadata inclusion, and hybrid/sparse modes | query.md |
| fetch | Retrieve vectors by ID or ID prefix | fetch.md |
| delete | Remove vectors by ID, ID prefix, or metadata filter | delete.md |
| range | Paginated cursor-based vector scan | range.md |
| Metadata Filtering | SQL-like filter syntax for restricting query and delete operations | filtering.md |
| Namespace | Index partitioning; implicit creation, listing, deletion, and reset | namespace.md |
| resumableQuery | Paginated search with server-side state; fetch result batches incrementally | resumable-query.md |
| Hybrid Indexes | Dense + sparse combined search with RRF/DBSF fusion and queryMode control | hybrid-indexes.md |
| Sparse Indexes | Exact keyword matching with BM25 and BGE-M3 sparse embedders | sparse-indexes.md |
| Embedding Models | Hosted text-to-vector models (BGE family, BM25) for automatic embedding | embedding-models.md |
| info / reset | Index statistics and namespace/index reset operations | info-reset.md |
| Python SDK (upstash-vector) | Python SDK differences, snake_case API, retry, and telemetry configuration | python-sdk.md |
resumableQuery
Paginated vector search with server-side state. Unlike standard query (single fixed-topK batch), resumable queries let you fetch additional result batches without re-running the full search.
Signature / Usage
import { Index } from "@upstash/vector";
const index = new Index();
// Start a resumable query
const { result, fetchNext, stop } = await index.resumableQuery({
vector: [0.1, 0.2, 0.3],
topK: 10,
maxIdle: 3600, // seconds; default 1 hour
includeMetadata: true,
filter: "genre = 'fantasy'",
});
// Process first batch
console.log(result); // initial topK results
// Fetch next batch
const nextBatch = await fetchNext(10); // fetch 10 more
// Terminate session
await stop();Options / Props
Payload:
| Name | Type | Required | Description |
|---|---|---|---|
vector | number[] | Yes* | Dense query vector |
sparseVector | object | Yes* | Sparse query vector |
data | string | Yes* | Text to embed; requires hosted embedding model |
topK | number | Yes | Number of results per batch |
maxIdle | number | No | Max idle time in seconds before auto-termination. Default: 3600 |
includeMetadata | boolean | No | Include metadata in results |
includeVectors | boolean | No | Include vector arrays in results |
includeData | boolean | No | Include the data field in results |
filter | string | No | Metadata filter expression applied for the entire session |
weightingStrategy | string | No | Sparse vector dimension weighting |
fusionAlgorithm | `'RRF' \ | 'DBSF'` | No |
Returned object:
| Name | Type | Description |
|---|---|---|
result | array | Initial batch of query results |
fetchNext | (k: number) => Promise<array> | Fetches the next k results continuing from current position |
stop | () => Promise<void> | Terminates the session and releases server resources |
Notes
- Always call
stop()when done to release server resources; otherwise the session expires aftermaxIdleseconds - Score values are normalized 0–1 regardless of similarity function
- Vector dimension must match the index dimension
- Available in Python, Go, and REST API with equivalent interfaces
Related
- query.md
- filtering.md
Sparse Indexes
Sparse vector indexes for exact keyword matching and information retrieval. Represent data in high-dimensional space where only a small fraction of dimensions have non-zero values.
Signature / Usage
// Upsert sparse vector directly
await index.upsert({
id: "doc-1",
sparseVector: {
indices: [10, 45, 200, 1500],
values: [0.8, 0.3, 0.5, 0.2],
},
metadata: { title: "Document Title" },
});
// Upsert raw text (with hosted BM25 or BGE-M3 model)
await index.upsert({
id: "doc-1",
data: "The quick brown fox",
metadata: { title: "Document Title" },
});
// Query sparse index
const results = await index.query({
sparseVector: {
indices: [10, 45],
values: [0.8, 0.3],
},
topK: 5,
includeMetadata: true,
});Options / Props
Sparse vector structure:
| Name | Type | Description |
|---|---|---|
indices | number[] | Non-zero dimension indices |
values | number[] | Corresponding non-zero values (same length as indices) |
Hosted embedding models for sparse indexes:
| Model | Space | Description |
|---|---|---|
BAAI/bge-m3 | 250,002 dimensions | Multi-functional, multi-lingual; contextual token weighting |
BM25 | 250,002 dimensions | Classic information retrieval. Parameters: k₁=1.2, b=0.75, avg doc length=32 tokens |
Notes
- Sparse indexes use inner product similarity; results may be exact matches (not approximate)
- Results may contain fewer than
topKitems when there is insufficient dimensional overlap between query and stored vectors - Maximum 1,000 non-zero dimensions per vector
- BM25 automatically maintains inverse document frequency (IDF) data for query-time weighting
- Sparse scores are not normalized 0–1 (unlike dense query scores)
Related
- hybrid-indexes.md
- embedding-models.md
- upsert.md
- query.md
@upstash/vector TypeScript SDK
Serverless vector database client for TypeScript/JavaScript. Provides full type safety for upsert, query, fetch, delete, range, and namespace operations against an Upstash Vector index.
Signature / Usage
import { Index } from "@upstash/vector";
// From environment variables (UPSTASH_VECTOR_REST_URL, UPSTASH_VECTOR_REST_TOKEN)
const index = new Index();
// Explicit credentials
const index = new Index({
url: "<UPSTASH_VECTOR_REST_URL>",
token: "<UPSTASH_VECTOR_REST_TOKEN>",
});
// With metadata type parameter for full type safety
type Metadata = { genre: string; year: number };
const index = new Index<Metadata>();Options / Props
| Name | Type | Description |
|---|---|---|
url | string | REST URL from Upstash console. Falls back to UPSTASH_VECTOR_REST_URL env var |
token | string | REST token from Upstash console. Falls back to UPSTASH_VECTOR_REST_TOKEN env var |
Notes
- Install:
npm install @upstash/vectororpnpm add @upstash/vector - Metadata type can be set at the index level (
new Index<Metadata>()) or per-command (index.upsert<Metadata>(...)) - Index-level type applies to all operations: query, upsert, fetch, range
- The index is eventually consistent — newly upserted vectors may not be immediately queryable
Related
- upsert.md
- query.md
- fetch.md
- delete.md
- range.md
- namespace.md
upsert
Adds new vectors to an index or updates existing ones. Vectors must match the index dimension.
Signature / Usage
// Single vector
await index.upsert({
id: "1234",
vector: [0.1, 0.2, 0.3, 0.4, 0.5],
metadata: { title: "Lord of The Rings" },
});
// Multiple vectors
await index.upsert([
{ id: "6789", vector: [0.6, 0.7, 0.8, 0.9, 0.9] },
{ id: "1234", vector: [0.1, 0.2, 0.3, 0.4, 0.5] },
]);
// With namespace
await index.upsert([...], { namespace: "my-namespace" });
// Text data (requires an index with a hosted embedding model)
await index.upsert({
id: "1234",
data: "The Lord of the Rings follows Frodo Baggins...",
metadata: { title: "Lord of The Rings" },
});Options / Props
Payload fields (VectorPayload):
| Name | Type | Required | Description |
|---|---|---|---|
id | `string \ | number` | Yes |
vector | number[] | Yes* | Dense vector values. Dimension must match index |
sparseVector | { indices: number[], values: number[] } | Yes* | Sparse vector for sparse/hybrid indexes |
data | string | Yes* | Raw text; embedding generated automatically by hosted model |
metadata | Record<string, unknown> | No | Arbitrary key-value metadata attached to the vector |
*Provide vector, sparseVector, or data depending on index type.
Options:
| Name | Type | Description |
|---|---|---|
namespace | string | Target namespace. Omit to use the default namespace |
Notes
- Returns
'Success'on completion - Upsert semantics: existing vectors with the same ID are overwritten
- Maximum 1,000 non-zero dimensions per sparse vector
- Namespaces are created implicitly on first upsert — no separate create step needed
Related
- fetch.md
- delete.md
- query.md
- namespace.md
Agents
The @upstash/workflow Agents API enables building durable AI agent pipelines — individual agents or multi-agent collaborations — with built-in reliability, observability, and integration with AI SDK and LangChain.
Signature / Usage
// Agents are built on top of the workflow context
// Refer to the Upstash Agents documentation for full setup:
// https://upstash.com/docs/workflow/agents/overviewSupported Architectural Patterns
| Pattern | Description |
|---|---|
| Prompt Chaining | Sequential LLM calls where each output feeds into the next |
| Evaluator-Optimizer | Iterative feedback loops that refine LLM outputs |
| Parallelization | Distributing tasks across multiple LLMs simultaneously |
| Orchestrator-Workers | A coordinator directing multiple worker agents |
Key Features
- Tool integration with Vercel AI SDK or LangChain
- Reliable agent invocation without serverless timeout concerns
- Enhanced debuggability via Upstash Console logs
- Durable execution: agent steps survive crashes and retries
Notes
- Agents API is available in
@upstash/workflow(JavaScript/TypeScript only; not yet available inworkflow-py) - Agent steps use the same
context.run(),context.call(), andcontext.invoke()primitives as standard workflows
Related
- overview
- context.run
- context.invoke
- serve-many
client.cancel()
Terminates one or more workflow runs. Supports direct run ID, filter-based, and bulk cancellation. Returns the count of canceled runs.
Signature / Usage
// Single run
await client.cancel("wfr_abc123")
// Multiple runs
await client.cancel(["wfr_abc123", "wfr_def456"])
// Filter-based
await client.cancel({
filter: { label: "my-label", fromDate: new Date("2024-01-01") },
count: 100,
})
// Bulk cancel all runs
await client.cancel({ all: true })Options / Props
When passing a filter object:
| Name | Type | Description |
|---|---|---|
filter.workflowUrl | string | Match runs by exact workflow URL |
filter.workflowUrlStartingWith | string | Match runs by URL prefix (mutually exclusive with workflowUrl) |
filter.label | string | Match runs by label |
filter.fromDate | `Date \ | number` |
filter.toDate | `Date \ | number` |
filter.callerIp | string | Filter by originating IP address |
filter.flowControlKey | string | Target runs with a specific flow control key |
count | number | Max runs to cancel per request; default: 100 |
all | boolean | Cancel all active runs when true |
Response
{ cancelled: number }Notes
- Bulk cancellation (
all: true) requires looping untilcancelled === 0to ensure all runs are terminated - Canceled runs do not trigger
failureFunctionand are not sent to the DLQ; they receive a "canceled" status
Related
- client
- client.logs
client.logs()
Retrieves paginated workflow run history with optional filtering by state, label, date range, and more.
Signature / Usage
// Basic retrieval
const { runs, cursor } = await client.logs()
// Filter failed runs
const { runs } = await client.logs({
filter: { state: "RUN_FAILED" },
})
// Paginated iteration
let cursor: string | undefined
do {
const result = await client.logs({ cursor, count: 50 })
process(result.runs)
cursor = result.cursor
} while (cursor)Options / Props
| Name | Type | Description |
|---|---|---|
cursor | string | Pagination token from a previous response |
count | number | Maximum runs to return per request |
filter | object | Filter criteria (see below) |
filter
| Name | Type | Description |
|---|---|---|
workflowRunId | string | Match a specific run ID |
workflowUrl | string | Match by workflow endpoint URL |
state | string | One of: RUN_STARTED, RUN_SUCCESS, RUN_FAILED, RUN_CANCELED |
label | string | Match by workflow label |
fromDate | `Date \ | number` |
toDate | `Date \ | number` |
messageId | string | Match by QStash message ID |
callerIp | string | Match by originating IP address |
flowControlKey | string | Match by flow control key |
Response
| Field | Type | Description |
|---|---|---|
runs | WorkflowRun[] | Array of workflow execution records |
cursor | `string \ | undefined` |
Related
- client
- client.cancel
client.notify()
Resumes workflows paused at context.waitForEvent() by delivering an event and optional payload. Called from outside a workflow (e.g., a webhook handler or API route).
Signature / Usage
import { Client } from "@upstash/workflow"
const client = new Client({ token: process.env.QSTASH_TOKEN! })
// Basic notify
await client.notify({
eventId: "payment-processed",
eventData: { amount: 100, status: "success" },
})
// With lookback (recommended for race condition prevention)
await client.notify({
eventId: "payment-processed",
eventData: { amount: 100 },
workflowRunId: "wfr_abc123",
})Options / Props
| Name | Type | Description |
|---|---|---|
eventId | string | Required. Identifier of the event to deliver; must match the eventId in context.waitForEvent() |
eventData | any | Data delivered to the waiting workflow's eventData field |
workflowRunId | string (optional) | Target a specific run; enables lookback — notification is stored even if sent before waitForEvent is reached |
Response
Returns Waiter[] — a list of notified workflow runs.
Notes
- Use
client.notify()from external code (webhooks, API routes outside the workflow); usecontext.notify()from inside a workflow - Providing
workflowRunIdis the recommended way to prevent race conditions
Related
- context.waitForEvent
- context.notify
- wait-for-event
client.trigger()
Initiates one or more new workflow runs and returns the assigned workflowRunId.
Signature / Usage
// Single workflow
const { workflowRunId } = await client.trigger({
url: "https://your-app.com/api/workflow",
body: { userId: "user_123" },
retries: 3,
delay: "5m",
})
// Batch trigger
const results = await client.trigger([
{ url: "https://your-app.com/api/workflow", body: { userId: "a" } },
{ url: "https://your-app.com/api/workflow", body: { userId: "b" } },
])Options / Props
| Name | Type | Description |
|---|---|---|
url | string | Required. Public workflow endpoint URL |
body | any | Payload accessible via context.requestPayload |
headers | object | HTTP headers forwarded to the workflow |
workflowRunId | string | Custom run ID (auto-prefixed with wfr_ if set) |
retries | number | Retry attempts on step failure; default: 3 |
retryDelay | `number \ | string` |
delay | `string \ | number` |
notBefore | number | Unix timestamp override for absolute scheduled start |
label | string | Label for dashboard/log filtering |
disableTelemetry | boolean | Disable telemetry collection for this run |
flowControl | object | Rate limiting and concurrency controls |
flowControl
| Name | Type | Description |
|---|---|---|
key | string | Logical grouping key for shared limits |
rate | number | Maximum requests per period |
parallelism | number | Maximum concurrent executions |
period | `string \ | number` |
Notes
- Trigger from server-side code only; do not expose
QSTASH_TOKENto the client retryDelayexpressions useretried(0-indexed attempt count), e.g.,"1000 * (1 + retried)"- Batch trigger returns an array of
{ workflowRunId }objects
Related
- client
- flow-control
- retries
Client
Lightweight, stateless client for programmatic workflow management. Used to trigger, cancel, notify, and inspect workflow runs from application code or external services.
Signature / Usage
import { Client } from "@upstash/workflow"
const client = new Client({
baseUrl: process.env.QSTASH_URL!,
token: process.env.QSTASH_TOKEN!,
})Options / Props
| Name | Type | Description |
|---|---|---|
token | string | QStash token from the Upstash dashboard |
baseUrl | string | QStash base URL; defaults to production if omitted |
Available Methods
| Method | Description |
|---|---|
client.trigger() | Start one or more workflow runs |
client.cancel() | Terminate active workflow runs |
client.notify() | Send an event to workflows paused at waitForEvent |
client.logs() | Retrieve workflow execution history |
client.getWaiters() | List workflows waiting for a specific event |
client.dlq.list() | List failed runs in the Dead Letter Queue |
client.dlq.restart() | Reprocess a failed run from the beginning |
client.dlq.resume() | Resume a run from the point of failure |
client.dlq.delete() | Remove an item from the DLQ |
client.dlq.retryFailureFunction() | Retry the failure handler for a run |
Notes
- A single
Clientinstance can be safely reused throughout the application; it is stateless QSTASH_URLandQSTASH_TOKENare provided by the Upstash dashboard or the local dev server
Related
- client.trigger
- client.cancel
- client.notify
- client.logs
context.call()
Makes an HTTP request as a workflow step. Upstash handles the request on the caller's behalf, allowing responses up to 12 hours without consuming compute resources.
Signature / Usage
const { status, body, headers } = await context.call<ResultType>("fetch-data", {
url: "https://api.example.com/data",
method: "POST",
body: JSON.stringify({ key: "value" }),
headers: { "Content-Type": "application/json" },
retries: 3,
timeout: 30,
})Options / Props
| Name | Type | Description |
|---|---|---|
stepName | string | Unique step identifier |
url | string | Target endpoint URL |
method | string | HTTP verb; defaults to "GET" |
body | string | Request payload as a string |
headers | Record<string, string> | Custom request headers |
retries | number | Number of retry attempts |
retryDelay | `number \ | string` |
timeout | number | Response wait duration in seconds |
flowControl | object | Rate limiting and concurrency controls (key, rate, parallelism, period) |
Response
| Field | Type | Description |
|---|---|---|
status | number | HTTP response status code |
body | T | Parsed JSON or raw string response body |
headers | Record<string, string> | Response headers |
Notes
- Returns responses for all status codes including non-2xx; inspect
statusto handle errors - Cannot reach
localhostor internal Upstash QStash services without a local tunnel - Use the generic type parameter
context.call<MyType>(...)for type-safe response bodies
Related
- context
- flow-control
- parallel-steps
context.invoke()
Launches another workflow and pauses the calling workflow until the invoked workflow finishes (success, failure, or cancellation). Requires both workflows to be registered in the same serveMany route.
Signature / Usage
import { createWorkflow, serveMany } from "@upstash/workflow/nextjs"
const childWorkflow = createWorkflow(async (context) => {
return await context.run("child-step", () => doWork())
})
const parentWorkflow = createWorkflow(async (context) => {
const { body, isFailed, isCanceled } = await context.invoke(
"invoke-child",
{
workflow: childWorkflow,
body: "input-data",
retries: 3,
}
)
})Options / Props
| Name | Type | Description |
|---|---|---|
stepName | string | Unique step identifier |
workflow | WorkflowObject | The target workflow created with createWorkflow() |
body | any | Payload passed as context.requestPayload to the invoked workflow |
headers | object | HTTP headers forwarded to the invoked workflow |
workflowRunId | string | Custom run ID (auto-generated if omitted, prefixed with wfr_) |
retries | number | Retry attempts on failure; default: 3 |
retryDelay | `number \ | string` |
flowControl | object | Rate limiting and concurrency controls |
Response
| Field | Type | Description |
|---|---|---|
body | TReturn | Return value from the invoked workflow |
isFailed | boolean | Whether the invoked workflow failed |
isCanceled | boolean | Whether the invoked workflow was canceled |
Notes
- Workflows can only invoke other workflows served together in the same
serveManydefinition - Use
createWorkflow()(notserve()) for workflows intended to be invoked
Related
- context
- serve-many