In this article
Choosing a library takes an afternoon. Every mainstream language has a good one, the documentation is decent, and a bot that replies to a command is fifteen lines in any of them.
So this step is not about the library. It is about the four things that make bots harder than they look — and every one of them is a consequence of the same fact: there is no conversation. There are only fragments arriving, and whatever you remember between them.
What you will be able to do after this step
- check_circle Say where a multi-step conversation actually lives.
- check_circle Design the dead ends, which is most of what makes a bot feel finished.
- check_circle Make sure a failure is visible to you rather than only to the user.
- check_circle Decide what to store, and keep considerably less than you can.
data_object There is no conversation, only updates
Your program is a function that gets called with a fragment: somebody sent this text, somebody pressed that button, somebody joined this group. Each call arrives with no memory of anything before it.
Everything that feels like a dialogue is therefore state you are keeping, keyed to a person and a chat, and read at the start of every fragment. "Ask for a name, then ask for an address" is not a sequence in your code — it is a note saying this person is on the address question, written down, and looked up when their next message arrives.
Which makes the location of that note the first architectural decision, and the one most often made by accident. Keeping it in memory is a decision even when it is the default, and its consequence is in the quiz below.
Your bot asks a question and waits for the answer. Where does the waiting live?
Nothing is waiting. Your bot sent a message and returned; some time later an unrelated-looking text message arrives, and the only way to know it is an answer to your question is a record you wrote saying so. Libraries offer helpers that make this look like a sequence, and they are keeping exactly that record for you — usually in memory by default. Understanding what is underneath matters, because every awkward case in the rest of this page is a case where the record and reality disagree: the person answered in a different chat, answered three days later, or answered a question you have since changed.
Your server restarts while people are halfway through a form. What do they experience?
If the state was in memory, it is gone, and the person's next message arrives as an ordinary text that matches no handler. From their side the bot simply stopped answering, which is indistinguishable from it being broken. Two things fix this and they are separate: store the state somewhere that survives a restart if the flow is worth resuming, and always have a fallback for input you were not expecting, so that something is said rather than nothing. The second is cheap and matters more — a bot that says "I have lost track, send /start to begin again" is a working bot, while a silent one is a dead one.
Two messages from the same person arrive a fraction of a second apart. What should you assume?
Modern bot frameworks handle updates concurrently, which is what allows one slow operation not to stall everybody else. The cost is that two updates from the same person can be in flight at once, both reading the state, both writing it, and the second one finishing first. The symptoms are strange: a value overwritten by an older one, a step skipped, a double submission. Together with the retry behaviour from Step 091, this is why operations should be safe to run twice and why anything that must not happen twice needs a guard of its own rather than an assumption about ordering.
alt_route Design the dead ends first
The difference between a bot that feels finished and one that feels broken is almost never the happy path, which works on the first afternoon. It is what happens everywhere else, and there are four places people leave it undefined.
Unexpected input at every step. You asked for a number and received a photograph, a sticker, an essay, or the word "hi". Every step needs a reply that says what is wanted, because the alternative is silence and silence reads as failure.
An escape from every state. One command that always works, from anywhere, and says so. People abandon halfway through things constantly; without a way out they are stuck in a form they no longer want, and the only remedy they can find is to block the bot.
Old buttons. Somebody will press a button on a message from three months ago, in a flow that no longer exists, possibly after your bot was rewritten. That press arrives like any other and should produce an explanation rather than an error — and the message should be edited so the stale buttons stop being tempting.
A timeout. A half-finished conversation should expire rather than sitting forever, both because the person has moved on and because a state record with no end is a small leak that becomes a large one.
bug_report A failure looks like nothing happening
When a web page errors, the person sees an error. When a bot errors, the person sees an absence — they sent something and nothing came back. They will conclude it is broken, or that they did something wrong, and a proportion of them will simply stop.
So two things are not optional. Catch failures and say something to the user, even if it is only that something went wrong and they should try again. And send the details somewhere you read — a private channel is the traditional answer and is genuinely good, because it puts your errors in the same application you are already looking at.
One caution that gets learned the hard way: an error channel with no rate limiting will, during an incident, produce thousands of identical messages and hit the sending limits from Step 091, at which point it stops working precisely when you need it. Group repeats, or send a count.
The related normal-failure case is being blocked. People block bots; it is ordinary and not an error. Sending to somebody who has blocked you fails, and the correct response is to mark them as unreachable and stop trying, not to retry forever. A broadcast loop that treats a block as a transient failure will spend its life retrying the people least likely to come back.
touch_app Try it now — five minutes
Four steps, and the last one is the one most bots have never had done to them.
- Write the fifteen-line version that replies to one command In whatever language you already use. It works immediately, which is the point — the difficulty is never here.
- Add a fallback that answers anything unexpected Before adding any features. It is the single change that most affects how the bot feels.
- Restart it mid-conversation and see what a user sees Then decide whether that flow needs state that survives a restart, or just a better message.
- Hand it to somebody who has not seen it Watch without helping. Every hesitation is a missing message, and you will have three of them in two minutes.
storage What to store, and how little you can get away with
Everything a person sends your bot arrives at your server. Keeping it is a choice, and the default in most projects is to keep everything because deleting requires a decision and storing does not.
Work out what you actually need and store that. A bot that answers questions usually needs nothing beyond the current conversation. A bot that takes orders needs the order, not the fourteen messages that produced it. A bot that moderates a group needs a record of actions, not a copy of the conversation it watched — and if privacy mode is off (Step 086), it is watching a great deal.
The reason is not abstract. A log of everybody's messages is a liability on the day anything goes wrong — a compromised server, a departing colleague, a request you did not expect. Data you never stored cannot leak, cannot be subpoenaed, and cannot be accidentally published. Data you stored for no reason is pure downside.
Two practical habits follow. Put an expiry on anything conversational, so the store empties itself rather than growing forever. And say what you keep, in the bot's own description or a command — one honest paragraph, which almost no bot has, and which is exactly what a cautious person looks for before using one.
help Common questions at this stage
Which language or library should I use?
The one you already write. Every widely used language has a mature library, they differ in ergonomics rather than capability, and none of them will be the reason your bot succeeds or fails. What is worth checking before committing is whether the library is actively maintained and whether its documentation covers the awkward parts of this page — conversation state, error handling, retries — rather than only the fifteen-line example.
Where should conversation state live?
Start in memory and move it when the flow is worth resuming after a restart. For a bot answering one-off questions, in-memory is correct and anything else is overhead. For a multi-step form, a payment, or anything a person has invested effort in, it belongs somewhere durable — a small database or key-value store is plenty. The mistake is not choosing wrongly; it is not noticing that a default was chosen for you.
How do I stop one slow operation blocking everyone?
Do not do slow work while handling an update. Acknowledge, put the job on a queue, and let a worker do it — the same structure as the webhook rule in Step 091, and for the same reason. Where the work is quick but external, use timeouts on every outbound call, because the default in many libraries is to wait indefinitely and one unresponsive service becomes a bot that has stopped answering anybody.
Can I develop against the same bot I run in production?
You will regret it. As Step 091 notes, a bot has one delivery mode, so a development copy fights the live one for updates. Beyond that, testing against a live bot means your experiments are visible to real users and a bad deployment posts into a real channel. A second bot with its own token, its own test channel and its own group costs nothing and is the setup every project arrives at eventually.
How do I send a message to thousands of users?
Slowly, and with a queue. Sending is paced by the platform, blocked users fail permanently, and a loop that iterates a list at full speed will be throttled into a mess where you cannot tell who received what. The working shape is a job that sends at a deliberate rate, records the outcome per recipient, marks blocked users as unreachable, and can be resumed after being interrupted. Building that properly the first time is quicker than debugging the naive version.
Does my bot need a database?
Less often than people assume. A bot that answers questions, converts something, or fetches from another service can be entirely stateless, which makes it dramatically easier to deploy, restart and reason about. Add storage when there is something you genuinely must remember between conversations — orders, subscriptions, moderation records — and prefer the smallest thing that holds it. A bot with a database it does not need has acquired backups, migrations and an outage mode for nothing.
At a glance
- 1State lives with you
There is no conversation, only messages; store each user's step yourself.
- 2Design dead ends
Handle unexpected input, an exit command and stale buttons.
- 3Store less
Keep only what the next step needs.
Updated