Reproduction steps
- Run a bot with 2 shards (
shard: [0, 2]andshard: [1, 2]) and keep both sessions connected. - Take a guild that belongs to the shard whose session identified second, so the guild is missing from the presence
guild_ids. - Kick the bot from that guild.
- Invite the bot to the guild again.
- Send a message in the guild.
Expected behaviour
The owning shard receives
GUILD_DELETE for the kick and GUILD_CREATE for the re-invite, and gets the events of the guild again.Actual behaviour
The owning shard receives neither
GUILD_DELETE nor GUILD_CREATE. Only the other shard gets GUILD_CREATE. After that no events of the guild reach the owning shard, so the bot stops responding there until that shard identifies again.Build information
Canary Desktop 2026.1026.22540, Web 2026.1027.182052, Windows 11 10.0.26300 (x64), Electron 44.9.1, Chrome 152.0.7977.167, Node 24.22.0, Locale en-GB
Platform
API
Evidence
Summary
With a sharded bot, the user presence holds only the guild IDs of the session that created it. When the bot is kicked from a guild of another shard and invited again, the owning shard never gets the guild back. It receives no events for that guild until it identifies again.Setup
- Bot with 2 shards (
shard: [0, 2]andshard: [1, 2]), about 2100 guilds - Both sessions stay connected for days, with resumes and the occasional fresh identify on either side
What happened
Guild1488115521506995110 belongs to shard 0 ((id >> 22) % 2 == 0). A moderator kicked the bot and invited it again a few minutes later.
- Shard 1 received
GUILD_CREATEfor the guild at 2026-10-28T09:17:44Z. The member'sjoined_atmatches to the second. - Shard 0 received neither
GUILD_DELETEfor the kick norGUILD_CREATEfor the re-join. - From then on no
MESSAGE_CREATEor other event of that guild reached shard 0, so the bot stopped responding there. - I restarted shard 0. A fresh identify builds the guild list from the database again, so the guild is back on the right session.
Cause
- The presence takes its
guild_idsfrom the session that starts it. That list is already filtered to the session's shard: - Later sessions of the same user adopt the running presence. Their guild IDs are never merged in:
- On kick,
handle_leave_guilddoes not find the guild inguild_ids, so noguild_leavegoes to the sessions: - The guild process still drops every session of the kicked user:
guild_state.erl#L194guild_state_member.erl#L125- The owning session keeps its
{Pid, Ref}entry inguilds, because the guild process is still alive and nothing told the session to remove it.
- On re-join,
handle_join_guildadds the guild and sendsguild_jointo all sessions:presence_connect.erl#L62- The session of the other shard has no entry, connects and gets
GUILD_CREATE. - The owning session finds its stale entry and returns without connecting:
session_connection_guild.erl#L44-L50
GUILD_DELETE.
Reproduced against the gateway code
I compiledfluxer_gateway at 7e30e95 in erlang:28.5.0.8-slim (NIF build skipped) and ran the steps above with the real modules: gateway_sharding, session_visits, presence_manager_shard with a real presence process, guild_state_member and session. Only the two bot sessions are stand-in processes that record what they receive.
== 1. identify filter (gateway_sharding)
shard 0 guilds: [1488115521506995110,1474874137937518680]
shard 1 guilds: [1485394316330348923,1564551479656722433]
== 2. presence guild_ids (session_visits:member_guild_ids + presence_manager_shard)
shard 1 then shard 0 get the same presence: true
presence guild_ids: [1485394316330348923,1564551479656722433]
== 3. kick from shard 0 guild 1488115521506995110 (presence leave_guild cast)
shard 0 session got: []
shard 1 session got: []
control: kick from shard 1 guild 1485394316330348923
shard 0 session got: [{guild_leave,1485394316330348923}]
shard 1 session got: [{guild_leave,1485394316330348923}]
== 4. guild process removes the kicked member's sessions (guild_state_member)
guild sessions before: [<<"other-user">>,<<"session-shard-0">>]
guild sessions after: [<<"other-user">>]
messages the shard 0 session got from this: []
== 5. re-invite (presence join_guild cast)
shard 0 session got: [{guild_join,1488115521506995110}]
shard 1 session got: [{guild_join,1488115521506995110}]
== 6. sessions handle guild_join (session:handle_cast + handle_info guild_connect)
shard 0 session: #{connect_started => false, guild_entry => {<0.88.0>,#Ref<...>}, state_unchanged => true}
shard 1 session: #{connect_started => true, guild_entry => no_entry_yet, state_unchanged => false}
erl -noshell -pa _build/default/lib/*/ebin -pa . -eval "repro:run(), halt()." after rebar3 compile.
Possible fixes
- Merge the session's
guild_idsinto the presence when a session adopts an existing presence. Removing them again when a session disconnects would also be needed, otherwise a guild that only one session knows can stay in the list. Either way the presence should hold the union of all its sessions' guilds. - In addition or instead,
handle_guild_connectcould check whether the guild process still has the session subscribed before it treats an existing{Pid, Ref}as connected.