The user sends a new message before the previous message is processed - an in-depth explanation of the four processing strategies for concurrent messages in a production environment: Reject, Enqueue, Interrupt and Rollback.
Double Texting(Double sending) means that the user sends a new message to the same thread before the current run is completed. In real chat applications, this situation is extremely common:
LangGraph Server passedmultitask_strategyParameters provide four strategies, allowing developers to choose the most appropriate concurrency processing method according to business scenarios.
On the same thread, Run 1 is still executing. What should I do when Run 2 arrives? These four strategies represent four different trade-offs.
When Run 1 is in progress, Run 2 is directly rejected and a 409 error is returned. Run 1 completes normally.
Run 2 enters the queue and waits. After Run 1 completes, Run 2 starts executing automatically. Both runs will complete.
Run 1 stops after the current node ends (keeping completed work) and Run 2 starts immediately. Run 1 status is interrupted.
Run 1 is stopped immediately anddelete(Undo all state), Run 2 starts from the thread's last clean state.
The simplest strategy: Run 1 rejects all new requests, returning HTTP 409 Conflict.
import httpx
thread = await client.threads.create()
config = {"configurable": {"user_id": "Test"}}
# 启动 Run 1
run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Add a ToDo to follow-up with DI Repairs.")]},
config=config,
)
# Run 2 使用 reject 策略 → 触发 HTTPStatusError 409
try:
await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Add a ToDo to mount dresser to the wall.")]},
config=config,
multitask_strategy="reject",
)
except httpx.HTTPStatusError as e:
print("Failed to start concurrent run", e)
After Run 1 is completed normally, the thread status only contains the first ToDo (mount dresser is not added).
SuitableDon't allow messages to be lostandYou can tell the user "please wait"scene. For example: financial transaction processing, form submission. The client needs to catch 409 and prompt the user to wait and try again.
Run 2 will not be rejected and will go into the waiting queue. After Run 1 is completed, the server automatically starts executing Run 2. Both runs will be executed and messages will not be lost.
thread = await client.threads.create()
# Run 1:立即开始
first_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Send Erik his t-shirt gift this weekend.")]},
config=config,
)
# Run 2:进入队列,等待 Run 1 完成
second_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Get cash and pay nanny for 2 weeks. Do this by Friday.")]},
config=config,
multitask_strategy="enqueue",
)
# 等待 Run 2 完成(Run 1 → Run 2 串行执行)
await client.runs.join(thread["thread_id"], second_run["run_id"])
SuitableEvery message must be processedscenario, and there is a logical sequence between messages (Run 2 needs to see the results of Run 1). For example: multi-step task entry, chat record saving. Note: If the user sends a "Supplementary Note", queuing ensures that the AI processes the original message first and then the supplement.
When Run 2 comes, Run 1 is paused after the current node has finished executing (it will not be killed immediately). The completed work of Run 1 is retained to PostgreSQL and the status becomesinterrupted;Run 2 starts execution immediately.
import asyncio
thread = await client.threads.create()
# Run 1 开始(查询 ToDo 摘要)
interrupted_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Give me a summary of my ToDos due tomorrow.")]},
config=config,
)
# 等 1 秒让 Run 1 执行一部分节点
await asyncio.sleep(1)
# Run 2 使用 interrupt 策略 → Run 1 在当前节点结束后停止
second_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Never mind, create a ToDo to Order Ham for Thanksgiving by next Friday.")]},
config=config,
multitask_strategy="interrupt",
)
await client.runs.join(thread["thread_id"], second_run["run_id"])
# 确认 Run 1 状态
status = (await client.runs.get(thread["thread_id"], interrupted_run["run_id"]))["status"]
print(status) # "interrupted"
Interrupt Reserved Run 1Completed nodes(if Run 1 has written to the Store, those writes will remain). Rollback will undo all effects of Run 1. If the side effects of Run 1 (writing to DB, sending notifications) are unacceptable, use Rollback; if part of the completed work is valuable, use Interrupt.
When Run 2 arrives, Run 1 is immediately stopped andDelete from database(Including all its status changes). Run 2 starts executing from the thread's last clean checkpoint, as if Run 1 never happened.
thread = await client.threads.create()
# Run 1:用户的原始意图
rolled_back_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Add a ToDo to call to make appointment at Yoga.")]},
config=config,
)
# Run 2:用户改变了主意
second_run = await client.runs.create(
thread["thread_id"], "task_maistro",
input={"messages": [HumanMessage(content="Actually, add a ToDo to drop by Yoga in person on Sunday.")]},
config=config,
multitask_strategy="rollback",
)
await client.runs.join(thread["thread_id"], second_run["run_id"])
# 验证 Run 1 已被删除
try:
await client.runs.get(thread["thread_id"], rolled_back_run["run_id"])
except httpx.HTTPStatusError:
print("Original run was correctly deleted") # ✓
Rollback will physically delete the records of Run 1. If Run 1 has triggeredIrreversible external side effects(such as sending emails, calling external APIs, deducting payments), these side effects will not be undone. Rollback only undoes the state inside LangGraph (checkpoints, writes in the Store).
| Strategy | Run 1 results | Run 2 results | Thread history | Applicable scenarios |
|---|---|---|---|---|
| Reject | Completed normally | ❌ Rejected (409) | Run 1 only | Concurrency is not allowed, the client can handle retries |
| Enqueue | Completed normally | ✅ Completed after queuing | Run 1 → Run 2 (serial) | Every message is important and depends on the order |
| Interrupt | ⏸ Interrupt (keep completed nodes) | ✅ Start now | Run 1 part + Run 2 | User wants to change direction, but does not need to undo work done |
| Rollback | 🗑 Delete (completely revoke) | ✅ Start from a clean state | Run 2 only | "I changed my mind, what I said before doesn't count" — Need a clean slate |
Universal chat application: Interrupt or Rollback (smoother user experience).Task management/workflow: Enqueue (each instruction needs to be executed).Finance/Forms:Reject (prevent duplicate submissions). In most dialogue assistant scenarios,RollbackMost in line with user intuition - "I have changed my mind, I pretended I didn't say anything in the previous post".