Fork fixes
Our fixes branch starts from the original project's last commit (97b3251, archived in March 2026). We used the other forks (DraftBot, davfsa, Melonly, LorittaBot, TicketsBot, PluralKit, WelcomerTeam…) as a catalogue of known bugs, not as code to merge: every bug was checked on the original code, against Discord's documentation and a capture of real Discord responses, then fixed with a test that fails without the fix.
The full list, with the evidence and where each bug was found, is in FIXES.md.
Build
#1Did not build with Go 1.23+: the pinnedgolang.org/x/netused ago:linknamenewer toolchains reject.
Rate limits
#2The global lock did nothing: written after a global 429, it was never read, and requests kept flowing into the limit.#3Any 404 under/webhooks/locked the route as an unknown webhook, including for a deleted webhook message.#5Every/channels/{id}request shared one queue, across all channels: locking many channels during a raid serialized them, and one exhausted channel put them all to sleep.#6/guilds/{id}/channelsshared one queue across every guild.#7An interaction's followup and original response ran in two queues, although Discord counts them together.#8Interactions waited on and spent the global limit, which Discord exempts them from.#9Renaming a channel or changing its topic shared the queue of every channel edit, and after its sub-limit 429, only the bucket's short reset was slept.#12The global limit was inferred frommax_concurrency, an undocumented heuristic that costs a/gateway/botrequest: off by default, replaced byBOT_RATELIMIT_OVERRIDES.#14The dedicated queue for deleting messages older than 14 days never applied, because of a comparison against the wrong path segment.#15The 429 the proxy makes up sentx-ratelimit-after, a header Discord never sends, instead ofX-RateLimit-Reset-After.
Requests and safety
#4The upstream URL was rebuilt from the decoded path: the keycap emoji#️⃣turned its#into a fragment, and the reaction reached Discord truncated. The hop between cluster nodes had the same bug.#10Invalid requests were not tracked, although 10,000 in ten minutes gets the IP banned: see Metrics.#11strings.SplitN(url, "?", 1)never split anything: a long query string value could exempt a request from the 401 lock.
Configuration
#13The Discord address was hardcoded:DISCORD_URLlets you target a simulator.#16A node advertised the address memberlist guessed, sometimes unreachable inside a container:CLUSTER_ADVERTISE_ADDR.
What we did not take
- davfsa's token bucket model, which sends again as soon as a token is back: its detection had no tests, and a client built on @discordjs/rest waits for the full refill anyway. The proxy waits the time Discord announces, which can be too long, never too short, whatever the bucket's model.
- The 250 ms pause after each reaction (DraftBot, PluralKit, inherited from eris): no capture supports it yet.
- Concurrent requests on one bucket (davfsa): a @discordjs/rest client sends one request per bucket at a time.
The original queues ignore the HTTP method, which makes them coarser than Discord's buckets: that can make requests wait longer than needed, never send into a limit, so we keep them that way.