LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 6 · Production
1 6-1 Creating
2 6-2 Connecting
3 6-3 Double Texting
4 6-4 Assistant
HomeLangGraphModule 6 · Production6-3 Double Texting
📓 Notebook 📖 Explained
LANGGRAPH MODULE 6 · LESSON 3

Double Texting

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.

1What is Double Texting and why it matters

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.

core issues

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.

2Overview of four strategies

🚫 Reject

When Run 1 is in progress, Run 2 is directly rejected and a 409 error is returned. Run 1 completes normally.

📋 Enqueue

Run 2 enters the queue and waits. After Run 1 completes, Run 2 starts executing automatically. Both runs will complete.

⏸️ Interrupt

Run 1 stops after the current node ends (keeping completed work) and Run 2 starts immediately. Run 1 status is interrupted.

🔄 Rollback

Run 1 is stopped immediately anddelete(Undo all state), Run 2 starts from the thread's last clean state.

3Strategy 1: Reject

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)
output
Failed to start concurrent run Client error '409 Conflict' for url '.../threads/.../runs'

After Run 1 is completed normally, the thread status only contains the first ToDo (mount dresser is not added).

Applicable scenarios

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.

4Strategy 2: Enqueue (queuing)

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"])
Thread status after execution is completed (both messages are processed)

Human: Send Erik his t-shirt gift this weekend.
AI: I've updated your ToDo list to send Erik his t-shirt gift this weekend.
Human: Get cash and pay nanny for 2 weeks. Do this by Friday.
AI: I've updated your ToDo list to get cash and pay the nanny for 2 weeks by Friday.
Applicable scenarios

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.

5Strategy Three: Interrupt

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"
thread final state

Human: Give me a summary of my ToDos due tomorrow.
← Run 1’s input message is preserved (work completed), but AI reply is not generated (interrupted at intermediate node)

Human: Never mind, create a ToDo to Order Ham for Thanksgiving by next Friday.
AI: I've added the task "Order Ham for Thanksgiving" to your ToDo list with a deadline of next Friday.
Key differences between Interrupt vs Rollback

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.

6Strategy 4: Rollback

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")  # ✓
Thread final status (only messages from Run 2)

Human: Actually, add a ToDo to drop by Yoga in person on Sunday.
AI: It looks like the task "Drop by Yoga in person" is already on your ToDo list with a deadline of November 19. Would you like me to update the deadline?
← The "call to make appointment" message never appears in the thread (Run 1 is completely deleted)
Notes on Rollback

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

7Four strategies comparison and selection guide

StrategyRun 1 resultsRun 2 resultsThread historyApplicable 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
Recommended selections from actual products

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