Discord Buttons Stop Working After a Restart — Make Them Stateless
Buttons and select menus live in the message forever. Someone can scroll up to a poll you posted last month, click an option, and Discord will dutifully send your bot an interaction. Your container will not remember posting it. Components survive in Discord's message history; the in-memory objects your library created to handle them do not survive a container being recreated — which happens on every deploy and every configuration change. The result is a button that looks alive and does nothing, or throws. The fix is to stop keeping handler state in memory and put it in the custom_id, which Discord documents as a "developer-defined identifier, 1-100 characters" that "is returned in the interaction payload sent when a user interacts with the component" (Discord docs).
Why it breaks
Most component tutorials have you build a view object, attach callbacks, and send it with the message. That object lives in your process. It holds the poll options, the user who started it, the page number — whatever the buttons need.
Then the container is recreated. New process, empty memory, and no knowledge of any message it sent before. The message is still sitting in the channel with its buttons intact, because Discord stores components with the message, not with your bot.
So the interaction arrives at a bot that has never heard of it. Depending on your library you get silence, an "interaction failed" in the client, or an exception in your logs.
This is the same class of problem as session state that doesn't survive a deploy, with one difference that makes it worse: the trigger is out of your control and arbitrarily delayed. A logged-out user signs in again immediately. A month-old button gets clicked whenever someone scrolls.
The custom_id is the fix
Discord's own documentation points at the answer. The field exists to be used: the docs note you can use it "flexibly to maintain state or pass through other important data", and it comes back to you on every interaction.
Two constraints to design around, both documented:
- 1 to 100 characters. That's the whole budget.
- It must be unique per component — "multiple components on the same message must not share the same
custom_id".
A workable scheme is a prefix and a couple of fields:
poll:41f9:b
Prefix routes the interaction to the right handler, the middle identifies the thing, the last identifies which button. Your handler parses it and works from that, holding no memory of having sent it.
Don't try to fit the whole state in. A hundred characters disappears quickly, and you shouldn't be storing a poll's options in a button anyway. Put an identifier in the custom_id and keep the actual record on a volume, the same way a bot keeps anything else it needs after a restart. The custom_id is a pointer, not a database.
Treat it as untrusted input
Two habits worth adopting, because the custom_id is not a private channel between you and yourself.
It's visible. Anyone who can read the message can read the component definition. Never put a token, an internal secret, or anything you'd mind a member of the server seeing.
It's not authorization. The custom_id tells you what was clicked, never who is allowed to. Check permissions in the handler at the moment of the click — the person who could use that button when you posted it may not be the person clicking it now, and may not even still have the role. Same reasoning as command permissions being a default rather than a boundary.
So parse defensively. If the identifier doesn't resolve, or the record has been deleted, that's an expected case rather than an exception.
Re-register your handlers at startup
Encoding state in the custom_id only helps if something is still listening for it.
Most libraries need you to re-attach component handlers when the process starts, rather than relying on the objects created when the message was sent. The details differ — check what your library calls this, and what it requires of the components — but the shape is always the same: on startup, tell the library "these identifiers belong to this handler", before any interaction can arrive.
If you skip it, the encoding works perfectly and nothing listens.
Handle the stale case on purpose
Once buttons outlive processes, they also outlive the things they refer to. A poll that ended, a ticket that closed, a message from a feature you removed.
Decide what each of those does:
- Reply, don't fail. "This poll has already ended" is an answer. An exception is a client-side "interaction failed" and a support question.
- Disable the components when the thing finishes. Edit the message to mark the buttons disabled once a poll closes. The interaction can still technically arrive, but almost nobody will send it, and the message stops looking live.
- Keep the reply ephemeral for stale cases, so an old thread doesn't collect a trail of error messages.
And still answer within the deadline — the three-second rule applies to component interactions too, and what an acknowledgement actually buys you has its own subtleties.
The Telegram version
Telegram inline keyboards have the same shape and a different budget. callback_data is what comes back to you, it's similarly small, and the buttons likewise persist in the chat long after your process has been replaced.
The design is identical: put an identifier in the callback data, keep the record on a volume, handle the case where the record has gone. The one Telegram-specific thing to get right alongside it is answering the callback query so the spinner stops.
A quick audit
Worth five minutes on any bot with components:
- Post a message with buttons. Then redeploy the app — or change an environment variable, which also recreates the container.
- Click the button. If it works, your handlers are stateless and re-registered. If it doesn't, you've found it.
- Check what the failure looks like from the user's side, because that's what people will report to you.
This is the same technique as testing anything else that has to survive a restart: make the restart happen deliberately rather than waiting to find out.
FAQ
Why do my Discord buttons stop working after the bot restarts?
The handler state lived in memory and the container was recreated. The message and its components still exist in Discord, so the interaction arrives at a process that knows nothing about it.
How do I make buttons work after a restart?
Encode what the handler needs into the custom_id, keep the real record in storage, and re-register your component handlers on startup.
How long can a custom_id be?
1 to 100 characters, and it must be unique among the components on a single message.
Can I put state in the custom_id?
Yes — Discord documents it as usable to maintain state or pass data. Keep it to identifiers, and never put secrets in it, since anyone reading the message can read it.
What should happen when someone clicks a very old button?
Reply that the thing has ended. Disable the components when a flow finishes so old messages stop looking interactive.
Does this apply to Telegram too?
Yes. callback_data behaves the same way, and inline keyboards outlive your process in exactly the same manner.
Wrapping up
The mental model that fixes this is that a component isn't a callback you registered — it's a small piece of data sitting in someone's chat history, which will be handed back to you at an unpredictable time, possibly by a different person, quite possibly after your code has changed. Design the handler for that: everything it needs either travels in those hundred characters or lives in storage it can look up. Then a redeploy stops being an event that quietly breaks the last month of messages.