openclaw - 💡(How to fix) Fix [telegram] webhook handler holds 200 ack until middleware completes — causes Telegram-side delivery timeouts on slow turns [2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#71392Fetched 2026-04-26 05:13:18
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
0
Timeline (top)
referenced ×3commented ×2closed ×1

The Telegram webhook handler in extensions/telegram/src/webhook.ts:280-378 only sends the 200 ack after the bot middleware (bot.on('message') → user-defined handlers) completes. For installations where the message handler does substantial work (agent dispatch, async task scheduling, multi-step routing), this can push the response time past Telegram's tolerance window, causing Telegram to mark the delivery failed and queue the message for retry.

The TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS = 10_000 ceiling combined with grammy's onTimeout: "return" does prevent unbounded blocking, but 10s is on the edge of Telegram's webhook delivery tolerance and adds unnecessary latency on every request.

The canonical webhook pattern (Telegram's own docs, Stripe webhooks, all major webhook receivers) is: ack 200 immediately, process the body asynchronously.

Root Cause

Without the ack-first pattern, every install whose bot handler does meaningful work hits a window where Telegram's queue grows during normal operation. Operators end up writing watchdog scripts to detect stuck pending_update_count and force re-delivery via deleteWebhookgetUpdates → replay — band-aid for what should be a clean handler pattern.

Fix Action

Fix / Workaround

The Telegram webhook handler in extensions/telegram/src/webhook.ts:280-378 only sends the 200 ack after the bot middleware (bot.on('message') → user-defined handlers) completes. For installations where the message handler does substantial work (agent dispatch, async task scheduling, multi-step routing), this can push the response time past Telegram's tolerance window, causing Telegram to mark the delivery failed and queue the message for retry.

  1. Configure a telegram channel via the bundled telegram extension with webhookUrl pointing at a publicly reachable endpoint
  2. Wire a bot handler whose work routinely takes >10s (e.g. an agent-dispatch handler that submits to a slower backend like an LLM via openclaw agent --json --message ...)
  3. Send a Telegram message
  4. Observe getWebhookInfo pending_update_count accumulates over time even when the system is otherwise healthy
  5. Time a direct POST to the local webhook endpoint:

Code Example

time curl -m 15 "http://127.0.0.1:18801/webhooks/telegram" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Telegram-Bot-Api-Secret-Token: <your-secret>" \
  -d '{"update_id":99999996,"message":{...}}'

---

// line 280
const handler = grammy.webhookCallback(bot, "callback", {
  secretToken: secret,
  onTimeout: "return",
  timeoutMilliseconds: TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS,
});

// line 375
await handler(body.value, reply, secretHeader, unauthorized);
if (!replied) {
  respondText(200);
}

---

respondText(200);
void handler(body.value, async () => {}, secretHeader, async () => {});
RAW_BUFFERClick to expand / collapse

Summary

The Telegram webhook handler in extensions/telegram/src/webhook.ts:280-378 only sends the 200 ack after the bot middleware (bot.on('message') → user-defined handlers) completes. For installations where the message handler does substantial work (agent dispatch, async task scheduling, multi-step routing), this can push the response time past Telegram's tolerance window, causing Telegram to mark the delivery failed and queue the message for retry.

The TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS = 10_000 ceiling combined with grammy's onTimeout: "return" does prevent unbounded blocking, but 10s is on the edge of Telegram's webhook delivery tolerance and adds unnecessary latency on every request.

The canonical webhook pattern (Telegram's own docs, Stripe webhooks, all major webhook receivers) is: ack 200 immediately, process the body asynchronously.

Reproduction

  1. Configure a telegram channel via the bundled telegram extension with webhookUrl pointing at a publicly reachable endpoint
  2. Wire a bot handler whose work routinely takes >10s (e.g. an agent-dispatch handler that submits to a slower backend like an LLM via openclaw agent --json --message ...)
  3. Send a Telegram message
  4. Observe getWebhookInfo pending_update_count accumulates over time even when the system is otherwise healthy
  5. Time a direct POST to the local webhook endpoint:
time curl -m 15 "http://127.0.0.1:18801/webhooks/telegram" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Telegram-Bot-Api-Secret-Token: <your-secret>" \
  -d '{"update_id":99999996,"message":{...}}'

Observed: ~10.0s response time on every request. Expected for an ack-first pattern: <100ms.

Diagnosis

In extensions/telegram/src/webhook.ts:

// line 280
const handler = grammy.webhookCallback(bot, "callback", {
  secretToken: secret,
  onTimeout: "return",
  timeoutMilliseconds: TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS,
});

// line 375
await handler(body.value, reply, secretHeader, unauthorized);
if (!replied) {
  respondText(200);
}

The await handler(...) blocks until middleware finishes (or 10s onTimeout fires). The reply callback isn't called until the middleware decides to call it — which for a heavy handler means at end-of-processing.

Proposed fix

Two options, ranked by impact:

Option 1 (recommended): Convert to fire-and-forget pattern.

respondText(200);
void handler(body.value, async () => {}, secretHeader, async () => {});

Trade-off: middleware errors no longer surface to Telegram (no 4xx/5xx). For Telegram webhook semantics this is fine — Telegram doesn't act on 4xx differently than 200, and 5xx triggers their retry curve. Logging is the right surface for handler errors, not HTTP status.

Option 2 (smaller, less surgical): Lower TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS to 2-3 seconds. Grammy still aborts the wait at the lower bound and ack happens faster. Middleware continues running in background after onTimeout returns. Less ideal because the awaited path still adds 2-3s on every request.

I'd offer a PR for option 1 if there's interest. The change is ~10 lines in webhook.ts. Tests in webhook.test.ts would need updating to assert ack-before-handler-completion.

Environment

  • openclaw 2026.4.15
  • Tailscale Funnel exposing local port 18801 to a public HTTPS endpoint
  • macOS 25.2.0
  • Telegram webhook with secret_token

Why this matters

Without the ack-first pattern, every install whose bot handler does meaningful work hits a window where Telegram's queue grows during normal operation. Operators end up writing watchdog scripts to detect stuck pending_update_count and force re-delivery via deleteWebhookgetUpdates → replay — band-aid for what should be a clean handler pattern.

extent analysis

TL;DR

Convert the Telegram webhook handler to a fire-and-forget pattern by sending a 200 acknowledgement immediately before processing the request asynchronously.

Guidance

  • Identify the current webhook handler implementation in extensions/telegram/src/webhook.ts and modify it to send a 200 response before calling the handler function.
  • Consider the trade-off of the proposed fix, where middleware errors will no longer surface to Telegram, but can be logged instead.
  • Review the TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS setting and consider lowering it to 2-3 seconds as an alternative, less ideal solution.
  • Update tests in webhook.test.ts to assert that the acknowledgement is sent before the handler completes.

Example

respondText(200);
void handler(body.value, async () => {}, secretHeader, async () => {});

Notes

The proposed fix assumes that the handler function can be safely called in a fire-and-forget manner, without blocking the main thread. Additionally, the fix may require updates to logging and error handling to ensure that middleware errors are properly handled.

Recommendation

Apply the fire-and-forget pattern by converting the webhook handler to send a 200 acknowledgement immediately, as it provides a more efficient and scalable solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [telegram] webhook handler holds 200 ack until middleware completes — causes Telegram-side delivery timeouts on slow turns [2 comments, 2 participants]