Skip to content

Commit 73552db

Browse files
authored
Merge pull request #714 from upstash/at-least-once
At-Least-Once Delivery Guarantee Documentation
2 parents 78454ec + 70a371e commit 73552db

4 files changed

Lines changed: 146 additions & 0 deletions

File tree

docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,7 @@
11461146
"qstash/features/batch",
11471147
"qstash/features/callbacks",
11481148
"qstash/features/dlq",
1149+
"qstash/features/at-least-once",
11491150
"qstash/features/deduplication",
11501151
"qstash/features/security"
11511152
]

llms-full.txt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6553,6 +6553,78 @@ curl https://qstash.upstash.io/v2/publish/...?qstash_token=<QSTASH_TOKEN>
65536553

65546554
Always keep your token safe and reset it if you suspect it has been compromised.
65556555

6556+
# At-Least-Once Delivery
6557+
Source: https://upstash.com/docs/qstash/features/at-least-once
6558+
6559+
QStash provides at-least-once delivery for all messages. This guarantees that no messages will be lost, even in the face of server crashes, or other unexpected problems.
6560+
6561+
In normal operation, each message is delivered once, excluding retries. However, in rare cases, QStash may deliver the same message more than once, even if your endpoint has already processed it successfully. This can happen when QStash cannot reliably determine whether the previous delivery attempt completed, so it retries the message to preserve at-least-once delivery guarantees.
6562+
6563+
A duplicate delivery can happen in a flow like this:
6564+
6565+
1. A message is published to QStash.
6566+
2. QStash attempts to deliver the message to the destination.
6567+
3. Before the request is completed, the QStash server shuts down unexpectedly.
6568+
4. When the server restarts, it cannot determine the final delivery status of the message.
6569+
5. To avoid losing the message, QStash delivers it again.
6570+
6571+
<Info>
6572+
True exactly-once delivery cannot be guaranteed in distributed systems under all failure scenarios.
6573+
6574+
Most production messaging systems therefore use at-least-once delivery together with idempotent handlers to prioritize reliability and prevent message loss.
6575+
6576+
To learn more about the underlying coordination challenge, see the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem).
6577+
</Info>
6578+
6579+
There are three common strategies to handle duplicate deliveries:
6580+
6581+
#### 1. Use an idempotency key
6582+
6583+
Because duplicate deliveries can occur, you can use an idempotency key to ensure that the an operation is executed only once.
6584+
6585+
Each QStash message includes a unique `Upstash-Message-Id` header, which you can use for this purpose.
6586+
6587+
For example, if your handler updates a database record, you can store the `Upstash-Message-Id` in the database along with the record. Before processing a message, you can check if the `Upstash-Message-Id` has already been processed. If it has, you can skip processing the message again.
6588+
6589+
An example implementation using Redis is shown below:
6590+
6591+
```typescript title="api/handler/route.ts"
6592+
import { Redis } from "@upstash/redis";
6593+
6594+
const redis = Redis.fromEnv();
6595+
6596+
export async function GET(request: Request): Promise<Response> {
6597+
const messageId = request.headers.get("Upstash-Message-Id");
6598+
6599+
const isNew = await redis.set(`processed:${messageId}`, "true", {
6600+
nx: true,
6601+
ex: 60 * 60 * 24,
6602+
});
6603+
6604+
if (!isNew) {
6605+
return Response.json({ message: "Message already processed" }, { status: 200 });
6606+
}
6607+
6608+
// critical business logic here
6609+
6610+
return Response.json({ message: "Message processed" }, { status: 200 });
6611+
}
6612+
```
6613+
6614+
#### 2. Design idempotent operations
6615+
6616+
You can also design your system so that applying the same operation multiple times does not change the final state. In this case, you may not need to store a separate idempotency key.
6617+
6618+
For example, if your handler sets a field to true, processing the same message multiple times has the same effect as processing it once. The final state remains true.
6619+
6620+
#### 3. Accept duplicates
6621+
6622+
In some cases, duplicate messages may be acceptable.
6623+
6624+
For example, if a message triggers a non-critical notification email, receiving the same message more than once may be tolerable, even if it results in multiple emails being sent.
6625+
6626+
This approach is only recommended when duplicate processing does not affect correctness, or any other critical behavior.
6627+
65566628
# Background Jobs
65576629
Source: https://upstash.com/docs/qstash/features/background-jobs
65586630

llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
- [Upsert URL Group and Endpoint](https://upstash.com/docs/qstash/api-reference/url-groups/upsert-url-group-and-endpoint.md): Add an endpoint to a URL Group
178178
- [API Rate Limit Response](https://upstash.com/docs/qstash/api/api-ratelimiting.md): This page documents the rate limiting behavior of our API and explains how to handle different types of rate limit errors.
179179
- [Authentication](https://upstash.com/docs/qstash/api/authentication.md): Authentication for the QStash API
180+
- [At-Least-Once Delivery](https://upstash.com/docs/qstash/features/at-least-once.md)
180181
- [Background Jobs](https://upstash.com/docs/qstash/features/background-jobs.md)
181182
- [Batching](https://upstash.com/docs/qstash/features/batch.md)
182183
- [Callbacks](https://upstash.com/docs/qstash/features/callbacks.md)

qstash/features/at-least-once.mdx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
title: "At-Least-Once Delivery"
3+
---
4+
5+
QStash provides at-least-once delivery for all messages. This guarantees that no messages will be lost, even in the face of server crashes, or other unexpected problems.
6+
7+
In normal operation, each message is delivered once, excluding retries. However, in rare cases, QStash may deliver the same message more than once, even if your endpoint has already processed it successfully. This can happen when QStash cannot reliably determine whether the previous delivery attempt completed, so it retries the message to preserve at-least-once delivery guarantees.
8+
9+
A duplicate delivery can happen in a flow like this:
10+
11+
1. A message is published to QStash.
12+
2. QStash attempts to deliver the message to the destination.
13+
3. Before the request is completed, the QStash server shuts down unexpectedly.
14+
4. When the server restarts, it cannot determine the final delivery status of the message.
15+
5. To avoid losing the message, QStash delivers it again.
16+
17+
<Info>
18+
True exactly-once delivery cannot be guaranteed in distributed systems under all failure scenarios.
19+
20+
Most production messaging systems therefore use at-least-once delivery together with idempotent handlers to prioritize reliability and prevent message loss.
21+
22+
To learn more about the underlying coordination challenge, see the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem).
23+
</Info>
24+
25+
There are three common strategies to handle duplicate deliveries:
26+
27+
#### 1. Use an idempotency key
28+
29+
Because duplicate deliveries can occur, you can use an idempotency key to ensure that the an operation is executed only once.
30+
31+
Each QStash message includes a unique `Upstash-Message-Id` header, which you can use for this purpose.
32+
33+
For example, if your handler updates a database record, you can store the `Upstash-Message-Id` in the database along with the record. Before processing a message, you can check if the `Upstash-Message-Id` has already been processed. If it has, you can skip processing the message again.
34+
35+
An example implementation using Redis is shown below:
36+
37+
```typescript title="api/handler/route.ts"
38+
import { Redis } from "@upstash/redis";
39+
40+
const redis = Redis.fromEnv();
41+
42+
export async function GET(request: Request): Promise<Response> {
43+
const messageId = request.headers.get("Upstash-Message-Id");
44+
45+
const isNew = await redis.set(`processed:${messageId}`, "true", {
46+
nx: true,
47+
ex: 60 * 60 * 24,
48+
});
49+
50+
if (!isNew) {
51+
return Response.json({ message: "Message already processed" }, { status: 200 });
52+
}
53+
54+
// critical business logic here
55+
56+
return Response.json({ message: "Message processed" }, { status: 200 });
57+
}
58+
```
59+
60+
#### 2. Design idempotent operations
61+
62+
You can also design your system so that applying the same operation multiple times does not change the final state. In this case, you may not need to store a separate idempotency key.
63+
64+
For example, if your handler sets a field to true, processing the same message multiple times has the same effect as processing it once. The final state remains true.
65+
66+
#### 3. Accept duplicates
67+
68+
In some cases, duplicate messages may be acceptable.
69+
70+
For example, if a message triggers a non-critical notification email, receiving the same message more than once may be tolerable, even if it results in multiple emails being sent.
71+
72+
This approach is only recommended when duplicate processing does not affect correctness, or any other critical behavior.

0 commit comments

Comments
 (0)