Since 2026-08-07 ~17:00 UTC, mautrix-discord bridges using a bot token fail to stay connected to Discord. The gateway connection dies about one second after READY, the bridge reconnect-loops every few seconds, and after ~1000 connection attempts Discord force-resets the bot token and emails you:
It appears your bot has connected to Discord more than 1000 times within a short time period. Since this kind of behavior is usually a result of a bug we have gone ahead and reset your bot's token.
Symptoms in the logs (journalctl -u matrix-mautrix-discord):
WRN wsapi.go:272:listen() > error reading from gateway wss://gateway.discord.gg/... websocket,
websocket: close 4002: Error while decoding payload.
WRN wsapi.go:224:Open() > Expected READY/RESUMED, instead got:
&discordgo.Event{Operation:9, Sequence:0, ...}
Eventually followed by close 4004: Authentication failed. once the token has been reset.
Notably, Matrix -> Discord relay keeps working (it uses Discord webhooks over REST); only Discord -> Matrix breaks, because that direction needs the gateway websocket. Getting a new token does NOT help; the new session dies the same way.
mautrix-discord does not use stock discordgo. Its go.mod replaces it with Beeper's fork:
replace github.com/bwmarrin/discordgo => github.com/beeper/discordgo v0.0.0-20260215125047-ccf8cbaa0a9f
In November 2025, the fork replaced the standard Op 1 gateway heartbeat with Discord's
client-internal Op 40 "QoS heartbeat" for ALL sessions, including bots
(beeper/discordgo PR #5, commit 20c39e9, "Update various headers and capability flags",
motivation: "more closely align the bridge's behavior with that of the first-party
Discord client"). The payload looks like:
{"op":40,"d":{"seq":123,"qos":{"active":true,"ver":26,"reasons":["foregrounded"]}}}On 2026-08-07, Discord's gateway started rejecting this payload on bot connections
with close 4002: Error while decoding payload, killing the session at the first
heartbeat (~1s after READY). discordgo then reconnects aggressively, which is what burns
through Discord's 1000-connection limit and triggers the token reset.
Verified by A/B testing a minimal bot against the gateway (same token, same intents):
| Library version | Heartbeat sent | Result |
|---|---|---|
| beeper/discordgo @ ccf8cbaa (v0.7.6 pin) | Op 40 QoS, ver 26 | 4002 at first heartbeat |
| same, with ver bumped 26 -> 28 | Op 40 QoS, ver 28 | 4002 at first heartbeat |
| beeper/discordgo @ HEAD (2026-08-03) | Op 40 QoS, ver 28 | 4002 at first heartbeat |
| ccf8cbaa + this patch | standard Op 1 | stable |
So as of 2026-08-08 this is not fixed anywhere upstream: not in mautrix-discord v0.7.6 (latest release), not on its main branch, and not at beeper/discordgo HEAD.
Independent confirmation: the DiscordBotClient / VencordDBCPlugin project (which also
sends client opcodes on bot tokens) hit the identical breakage on Aug 7. Their finding
matches: the bot gateway now rejects a non-null qos object (they fixed it by sending
qos: null; sending the standard Op 1 is the cleaner fix for a pure bot).
See aiko-chan-ai/VencordDBCPlugin#3
mautrix-discord-op1-heartbeat.patch (in this gist) applies to beeper/discordgo at
commit ccf8cbaa0a9f (the exact commit mautrix-discord v0.7.6 pins). It makes bot
sessions send the standard Op 1 heartbeat and leaves user-token sessions (Beeper's
puppeting use case) on the QoS heartbeat:
func (s *Session) newHeartbeatOp(seq int64) interface{} {
if s.IsUser {
return newForegroundedQosHeartbeatOp(seq)
}
return heartbeatOp{Op: 1, Data: seq}
}This keeps the official dock.mau.dev/mautrix/discord:v0.7.6 image and overlays a
patched binary. Paths below assume the spantaleev/matrix-docker-ansible-deploy layout
(/matrix/mautrix-discord/...); adjust to taste.
mkdir -p /matrix/mautrix-discord/patched-build
cd /matrix/mautrix-discord/patched-build
git clone --depth 1 --branch v0.7.6 https://github.com/mautrix/discord.git src
git clone https://github.com/beeper/discordgo.git discordgo
cd discordgo
git checkout ccf8cbaa0a9f
git apply /path/to/mautrix-discord-op1-heartbeat.patchThe official binaries are statically linked against libolm; Alpine 3.23 no longer ships
a static olm, so build dynamically and ship libolm.so.3 alongside (step 3):
cd /matrix/mautrix-discord/patched-build
docker run --rm -v "$PWD":/build -w /build/src golang:1-alpine3.23 sh -c '
apk add --no-cache git ca-certificates build-base olm-dev &&
go mod edit -replace github.com/bwmarrin/discordgo=/build/discordgo &&
go build -o /build/mautrix-discord .'docker run --rm -v "$PWD":/out alpine:3.23 sh -c \
'apk add --no-cache olm && cp -L /usr/lib/libolm.so.3 /out/libolm.so.3'Edit the docker create line (for matrix-docker-ansible-deploy:
/etc/systemd/system/matrix-mautrix-discord.service, back it up first) and add:
--mount type=bind,src=/matrix/mautrix-discord/patched-build/mautrix-discord,dst=/usr/bin/mautrix-discord,ro
--mount type=bind,src=/matrix/mautrix-discord/patched-build/libolm.so.3,dst=/usr/lib/libolm.so.3,ro
Then:
systemctl daemon-reload
systemctl restart matrix-mautrix-discordjournalctl -u matrix-mautrix-discord -f
# no more "close 4002" every ~45s; send a message in a bridged Discord channel
# and confirm it arrives in MatrixCaveat: if you deploy via ansible, the next playbook run rewrites the unit file and silently removes the mounts. Treat this as a stopgap until it is fixed upstream.
mautrix-discord v0.7.5 pins an older fork commit (f23a8518, June 2025) that still
sends the standard Op 1 heartbeat, verified working against the current gateway.
Safe schema-wise: v0.7.6 migrates the bridge DB to version 24, but that migration is
marked compatible with v19+, and v0.7.5 understands compat level 19. Check yours:
SELECT version, compat FROM version; -- in the mautrix-discord database; expect 24 | 19Then point the service at dock.mau.dev/mautrix/discord:v0.7.5 (in
matrix-docker-ansible-deploy: matrix_mautrix_discord_docker_image override, or edit the
unit file) and restart. You lose the few v0.7.6 additions (federation thumbnail endpoint,
new upload fields).
- If your token was already reset, get a new one from the Discord developer portal and
re-login: as the bridge's Matrix user, DM the bridge bot
login-token bot <TOKEN>. - Do not leave an unpatched bridge running: it will burn ~1000 connects in a few hours
and get the fresh token reset again.
systemctl stop matrix-mautrix-discorduntil you have a fix in place.