Your Bot Greets Every Server Again on Every Restart
If your bot posts a welcome message when it's added to a server, there's a good chance it posts that message to every server it's in, every time it restarts. The reason is that the event people hook for "joined a server" isn't only sent for joins. Per Discord's documentation, Guild Create is sent in three cases: "when a user is initially connecting, to lazily load and backfill information for all unavailable guilds sent in the Ready event", "when a Guild becomes available again to the client", and "when the current user joins a new Guild" (Discord docs). Only the third is a join — and telling them apart requires remembering which servers you already knew about, across restarts.
Why this shows up here in particular
On a laptop you restart your bot a handful of times while developing, in one test server. The bug is invisible.
Deployed, your container is replaced on every deploy and every configuration change, not just crashes. Each replacement means a fresh gateway connection, which means Guild Create for every guild you're in. So the more you ship, the more often every server hears from you.
At small scale that's embarrassing. At a few hundred servers it's also a burst of API calls in a few seconds, and any that fail — a channel you can't post in returns a 403 — count toward a limit with a harsh penalty.
The fix, and why the obvious version fails
Keep a record of guilds you've already seen. On Guild Create, if the ID is new to you, it's a real join.
The catch is where that record lives. A set in memory is empty after every restart, so every guild looks new — which is precisely the bug you're trying to fix, reintroduced. The record has to survive a container replacement, which means a volume:
CREATE TABLE guilds (
guild_id TEXT PRIMARY KEY,
joined_at INTEGER NOT NULL
);
async def on_guild_create(guild):
is_new = db.execute(
"INSERT OR IGNORE INTO guilds (guild_id, joined_at) VALUES (?, ?)",
(guild.id, int(time.time())),
).rowcount > 0
if is_new:
await welcome(guild)
INSERT OR IGNORE plus rowcount gives you "was this actually new" atomically, which also makes the handler safe to run twice.
Some libraries expose a separate join-only event that handles this for you. If yours does, use it — and still keep the table, because you'll want it for the next two sections.
What a real join should do
Three things, in this order.
Check you can actually work. Verify the permissions your bot needs now, rather than discovering them when a command fails silently a week later. If something's missing, say so in the welcome message — "I need Send Messages in the channels you want me to post in" is far more useful than a later Missing Permissions error nobody sees.
Find somewhere you're allowed to post. There's no guaranteed welcome channel. Look for a system channel, then a general channel you have permission to write in, and stop at the first one that works. If there's nowhere, consider messaging whoever added you instead — and if that fails too, log it and move on rather than retrying.
Post once, in one channel. A bot that introduces itself in every channel is a bot that gets removed.
Say what you are in one message. What the bot does, the one command to try first, and where to get help. Not a wall of configuration.
Don't do heavy work per guild on connect
Even with the join check right, be careful what runs during Guild Create generally.
On every reconnect this handler fires once per guild in quick succession. Anything expensive inside it — an API call, a database write per guild, a permissions audit — becomes a burst proportional to your size, at exactly the moment your process is also doing its startup work.
Keep the connect path cheap. If you need per-guild maintenance, do it from your background loop at a leisurely rate, not on connect.
Handle removal too
When your bot is removed from a server, delete that server's data.
Two reasons, and both matter:
- Storage. Per-guild rows accumulate, and volume size has an account-wide consequence — exceeding your disk limit stops every app you run.
- It's the right default. Keeping configuration and history for a community that removed you is poor practice, and "we deleted your data when you removed the bot" is a good thing to be able to say.
Delete the guild row and anything keyed to it in the same transaction as handling the removal event.
Prefer defaults over setup flows
The temptation after a join is a configuration wizard. Resist it — the moment someone adds a bot is the moment they have the least patience.
Ship sensible defaults so the bot is useful immediately, and make configuration something a server owner can change later if they want to. Store per-guild settings keyed by guild ID in the same database, with the default applied when there's no row.
A bot that works instantly and can be adjusted beats one that must be configured before it does anything.
FAQ
Why does my bot post its welcome message repeatedly?
You're handling Guild Create, which also fires when your bot connects and lazily loads every guild it's already in — so every restart looks like a batch of joins.
How do I detect an actual join?
Keep a record of guild IDs you've seen and treat unknown IDs as new. Some libraries also expose a join-only event.
Why doesn't an in-memory set work?
It's empty after every restart, so every guild appears new — which recreates the original bug. The record must live on a volume.
Where should the welcome message go?
The first channel you have permission to post in, once. If there isn't one, message whoever added the bot, then give up quietly.
What should I do when my bot is removed?
Delete that server's data. It keeps your storage bounded and it's the right default for a community that removed you.
Should I ask for configuration on join?
No. Ship working defaults and let owners change them later — nobody wants a wizard the moment they add a bot.
Wrapping up
This is a nice example of a bug that only exists in deployment: the code is fine, the event is fine, and the assumption — "this fires when I join a server" — is only mostly true. What makes it bite here is how often containers are replaced, and what makes the naive fix fail is the same thing. Once the record of known guilds lives somewhere a restart can't reach, the join path becomes genuinely rare, which is exactly when it's worth doing properly.