Skip to main content

What is a queue?

A queue holds items in order: the first item added is the first one taken out (FIFO, or First In First Out), like a line of people waiting for service. In software, a queue lets one part of your system hand work to another so the producer doesn’t have to wait for the work to finish. Redis works well for queues. Its list and sorted set types map onto queue operations, and operations are atomic, so multiple producers and consumers can share a queue without stepping on each other. With Upstash Redis you reach it over HTTP, so the same queue works from a serverless function or a long-running worker. This tutorial starts with a plain FIFO queue and builds up to a job queue that supports delays, priorities, and retries.

Database Setup

Create a Redis database using the Upstash Console. If this is your first one, the Getting Started guide walks through creating a database and finding its credentials. Then copy the REST URL and token into your .env file:

Installation

Then create a client. Redis.fromEnv() reads the two variables above automatically:

A basic FIFO queue

A Redis list is all you need for a simple queue. You push items onto one end and pop them off the other. The convention is to enqueue on the left with LPUSH and dequeue on the right with RPOP, so the oldest item is always the next one out.
OperationCommandDescription
EnqueueLPUSH queue taskAdd a task to the head of the list
DequeueRPOP queueRemove and return the oldest task
PeekLRANGE queue -1 -1Look at the next task without removing it
LengthLLEN queueHow many tasks are waiting
To peek at the next task without removing it, read the tail with LRANGE. This is useful for monitoring or for deciding whether to process:
This is enough for fire-and-forget work, but it has a flaw: if your consumer pops a task and then crashes before finishing it, that task is lost. The next sections fix that.

A reliable queue

To avoid losing tasks, don’t fully remove a task until it has been processed. Redis’s LMOVE command (the modern replacement for RPOPLPUSH) atomically moves a task from the main queue to a “processing” list in a single step:
Because the task stays in processing while it is being worked on, a crashed consumer doesn’t cause data loss. A periodic recovery job can scan processing for tasks that have been stuck too long and move them back to tasks.

Consuming the queue

A consumer reads from the queue in a loop. With a TCP Redis client you might use a blocking pop (BRPOP) to sleep until a task arrives, but @upstash/redis talks to Redis over HTTP, so it doesn’t expose blocking commands. Instead, poll with RPOP and back off with a short sleep when the queue is empty:
Polling works well for a long-running worker, but in short-lived serverless functions you usually don’t want to run a loop at all. For event-driven delivery, consider Upstash QStash, which pushes messages to your HTTP endpoint instead of making you poll. We come back to this at the end.

Going further: a delayed, prioritized job queue with retries

Plain lists are FIFO and immediate. Real job systems usually need a few more things:
  • Delays: run a job later, like a reminder in an hour, instead of right away.
  • Priorities: let urgent jobs go ahead of routine ones.
  • Retries: when a job keeps failing, retry it a few times and then set it aside rather than retrying forever.
A sorted set handles all three. It keeps members ordered by a numeric score. If the score is the time a job becomes due, the member with the lowest score is always the next job to run, and jobs scheduled for the future sit untouched until their time comes.

The schedule: score = “run at” timestamp

Claiming due jobs atomically

A worker should only pick up jobs whose runAt is in the past, and when several workers run at once, no two should claim the same job. A Lua script handles this: it reads the due jobs and removes them in one step, so there is no race between the read and the remove.

The worker: process, retry, or dead-letter

When a job fails, we re-schedule it with an exponential back-off delay and increment its attempt count. Once it runs out of retries, it goes to a dead-letter queue: a separate list of jobs to look at later, rather than retrying them again.
This gives you a job queue that schedules work for later, runs urgent jobs first, retries failures with back-off, and moves jobs that keep failing to a dead-letter queue, all on a single sorted set.

Inspecting the dead-letter queue

Failed jobs live in a list, so checking on them is straightforward. You can wire this into a dashboard or an alert:

Wrapping up

Redis covers queues at several levels, all in the same database:
  • Lists (LPUSH / RPOP) for simple FIFO queues, plus LMOVE for reliability and BRPOP for blocking consumers.
  • Sorted sets (ZADD / ZRANGEBYSCORE) for delayed and prioritized scheduling.
  • Lua scripts (EVAL) to claim work atomically across many concurrent workers.
Because Upstash Redis is serverless and accessed over HTTP, you can produce jobs from serverless or edge functions and consume them from wherever your workers run.

When to reach for QStash instead

The job queue above assumes you have a worker process polling Redis. In a fully serverless setup you often don’t want to keep a worker running. That is where Upstash QStash comes in: instead of pulling jobs from a list, you publish a message and QStash pushes it to your HTTP endpoint, with retries, scheduling, and delays handled for you.
A rough rule of thumb: use the Redis patterns in this guide when you control the consumer and want full control over how jobs are stored and claimed. Reach for QStash when you’d rather not run a worker at all and just want jobs delivered to an endpoint. See the QStash documentation to get started.