fix: handle Discord typing indicator 429 gracefully - #22398
Conversation
The typing indicator loop (send_typing) ran every 8s and died on any exception, including Discord 429 rate limits. Once a 429 killed the loop, the indicator never restarted — and the raw exception bounce could cascade into broader gateway instability. Changes: - Bump sleep interval from 8s to 12s (typing light lasts ~10s) - On 429: extract retry_after, log a warning, sleep the backoff, and continue the loop - On non-rate-limit errors: log debug and return (unchanged behaviour)
There was a problem hiding this comment.
Pull request overview
This PR updates DiscordAdapter.send_typing() to make the Discord typing-indicator background loop more resilient, specifically to avoid permanently losing the typing indicator when Discord rate-limits the typing endpoint.
Changes:
- Increased typing refresh cadence from 8s to 12s to reduce endpoint pressure while keeping the indicator mostly continuous.
- Added rate-limit-aware handling in the typing loop: attempt to extract
retry_after, sleep, and continue rather than exiting immediately. - Expanded docstring to describe the intended rate-limit behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Rate-limit handling: if a 429 is encountered, the loop logs a | ||
| warning, sleeps for the ``retry_after`` duration (or a sensible | ||
| default), and continues — it does NOT die on a single rate-limit | ||
| hit. Only CancelledError (from stop_typing) stops the loop. |
| # Don't die on 429 — backoff and continue | ||
| retry_after = self._extract_discord_retry_after(e) | ||
| if retry_after is not None: | ||
| logger.warning( | ||
| "Typing indicator rate-limited for %s; retrying in %.1fs", | ||
| chat_id, retry_after, | ||
| ) | ||
| else: | ||
| logger.debug( | ||
| "Discord typing indicator failed for %s: %s", | ||
| chat_id, e, | ||
| ) | ||
| return | ||
| await asyncio.sleep(retry_after) | ||
| continue |
| else: | ||
| logger.debug( | ||
| "Discord typing indicator failed for %s: %s", | ||
| chat_id, e, | ||
| ) | ||
| return | ||
| await asyncio.sleep(retry_after) |
| # Don't die on 429 — backoff and continue | ||
| retry_after = self._extract_discord_retry_after(e) | ||
| if retry_after is not None: | ||
| logger.warning( | ||
| "Typing indicator rate-limited for %s; retrying in %.1fs", | ||
| chat_id, retry_after, | ||
| ) | ||
| else: | ||
| logger.debug( | ||
| "Discord typing indicator failed for %s: %s", | ||
| chat_id, e, | ||
| ) | ||
| return | ||
| await asyncio.sleep(retry_after) | ||
| continue | ||
| await asyncio.sleep(12) |
|
Just a note from deploying this fix — The typing indicator 429 handling works great. Logs show graceful retries instead of crashes, no issues there. One thing I ran into though — on my setup the gateway got stuck in a restart loop. When I switched it to Just make sure to keep |
Summary
The typing indicator loop (
send_typing) ran every 8s and died on anyexception, including Discord 429 rate limits. Once a 429 killed the
loop, the indicator never restarted — and the raw exception bounce
could cascade into broader gateway instability.
Changes
the indicator mostly continuous with less endpoint pressure)
retry_afterfrom theexception, log a warning, sleep the backoff, and continue the loop
Test Plan
python3 -c "import ast; ast.parse(...)"concurrent channel load
Closes #N/A