Why a bot on your own machine keeps dropping offline
A bot started from a terminal belongs to that terminal. Close the window or lose the SSH session and the shell sends a hangup signal to everything it started. A sleeping laptop suspends the process and its network connection. An operating system update reboots the machine at an hour nobody chose, and nothing starts the bot again. A connection that changes its public address drops every open socket. And a crashed bot stays down until someone notices.
tmux and screen keep a session alive after you disconnect, and nohup ignores the hangup signal. Each fixes one of those problems, and neither restarts anything.
What 24/7 requires is a supervised process: something other than you that starts the bot at boot, notices when it exits and starts it again.
- systemd: Restart=on-failure, which its manual recommends for long-running services, on a unit enabled to start at boot.
- Docker: a restart policy of unless-stopped or always. It applies only once a container has stayed up for 10 seconds, so one that dies straight after starting is not retried.
- pm2 for Node: pm2 startup writes a boot script for your init system, and pm2 save records which apps to bring back.
- Test it: kill the process by hand, reboot once, and check the bot came back both times.
Restart slowly: Discord's daily login allowance
Instant restarts are the wrong default for a Discord bot. Every new process has to identify with the gateway, and Discord allows 1,000 identifies in 24 hours across all of a bot's shards. Resuming an existing session does not count. At the limit, Discord terminates every active session, resets the bot token and emails the owner.
A day has 86,400 seconds, so a bot that logs in and crashes more often than about once every 86 seconds, all day, spends the allowance and loses its token. A handler that throws on a common event is exactly that bot.
Supervisors have their own guard. systemd waits 100 ms between restarts by default and will not start a unit that has started more than five times in ten seconds, so a fast-failing bot is left stopped. The fix for both is a growing delay: short after the first crash, longer after each that follows, capped at a few minutes. If your supervisor only offers a fixed delay, have the bot sleep before logging in when its last few starts were recent.
- Docker stops a container with SIGTERM and, after 10 seconds on Linux, SIGKILL, which nothing can catch. Close the Discord client and finish writing files inside that window.
- Node's default SIGTERM handler exits at once. Install your own listener and that default is removed, so your handler must exit when it is done.
Gateway reconnects are normal, and the library handles them
A Discord bot does not wait for inbound traffic. It holds an outbound WebSocket to the gateway and sends heartbeats at an interval Discord sets, each one acknowledged. If an acknowledgement has not arrived by the next heartbeat, the documentation calls the connection failed or 'zombied', and the client closes it and resumes: it reconnects to the resume URL it was given with its session ID and last sequence number, and Discord replays what it missed. Discord can also send a Reconnect event, or answer Invalid Session, after which the client starts a new session.
None of that belongs in your code. discord.py's run() reconnects by default, and discord.js reports the same work through its shardReconnecting and shardResume events, which are worth logging. A few failures are deliberately not retried: a bad token, intents the bot may not request, and sharding errors. discord.js calls these unrecoverable close codes and discord.py leaves bad tokens unhandled, so the client stops and the log says why.
- Gateway sends are rate limited to 120 events per connection every 60 seconds, and an app over that is disconnected immediately. Loops that update presence or request members are the usual cause.
- One shard holds at most 2,500 servers, and a bot in 2,500 or more must shard. discord.js's ShardingManager gives each shard its own process or worker, client and caches, so memory grows with the shard count.
Privileged intents, verification, and a rule that changed
Intents decide which events Discord sends. Three are privileged: Server Members, Presence and Message Content. Each must be switched on in the Developer Portal and requested in code; discord.py's documentation notes that the portal alone is not enough. Request one the portal has not enabled and the connection is refused, which the libraries treat as unrecoverable.
Without Message Content, the content, embeds, attachments, components and polls of other people's messages arrive empty. The exceptions are messages the bot sent, direct messages to it, messages that mention it, and the message a context menu command is used on. Slash commands do not depend on it, so a bot built on them often needs no privileged intent at all.
Many tutorials still describe the old access rules. Access now depends on users, not servers: below 10,000 users an app enables privileged intents itself in the portal, and past 10,000 unique users continued access needs a review. Separately, and unchanged, an app must be verified before it can grow past 100 servers. They are two processes, so a public bot should plan for both.
- Ask for the fewest intents that work. Each one is traffic to receive, parse and often cache.
- With the members intent on, discord.py fetches every server's member list at startup by default (chunk_guilds_at_startup), which delays readiness and fills memory. Turn it off unless you need complete lists.
- Presence delivers members' status and activity changes. Few bots use it, so question it first.
Tokens, keys and the first ten minutes after a leak
Keep the bot token and every API key in environment variables, and add your local .env to .gitignore before the first commit, not after. Discord's getting-started guide says never to check the token into version control, and model SDKs follow suit: Anthropic's Python SDK reads ANTHROPIC_API_KEY from the environment by default. Add .env to .dockerignore too, or copying the project into an image copies the file.
Leaked tokens get used quickly because finding them is cheap: they have recognisable shapes, and public commits are visible to everyone. GitHub's secret scanning treats Discord bot tokens as a partner pattern, so one found in a public repository is reported to Discord, and push protection can block the push where it is enabled. People with worse intentions read the same commits. BotFather says as much of Telegram tokens: anyone holding one can control your bot.
Deleting the file does not help, because the earlier commit still holds it. Reset first, clean up afterwards.
- Discord: on your application's Bot page in the Developer Portal, use Reset Token. The new token is shown once, so paste it straight into your host's environment variables.
- Telegram: send /token to BotFather, its documented route when a token is compromised.
- Rotate everything that sat beside it. The same .env usually held a model API key, a database password or a webhook secret.
- Restart, confirm the bot connects, then check what it did while exposed: your servers' audit logs and your model provider's usage page.
Sizing: what 512 MB runs and what genuinely needs more
A bot's memory has three parts: the runtime and libraries, roughly fixed; caches, which grow with the servers, members and messages it sees; and transient work, which spikes while a request is handled. A typical discord.py or discord.js bot answering commands, managing roles and calling a few web APIs is mostly the first two, and fits comfortably in 512 MB.
Size from measurement. Memory a minute after login says little; the figure after a week in all its servers, including the busiest hour, is the one to plan against. Both libraries can bound the caches that drive growth. discord.py keeps the last 1,000 messages by default through max_messages and shapes the member cache with member_cache_flags. discord.js caps caches with makeCache and prunes them with sweepers.
Half a vCPU suits a bot that mostly waits on the network. CPU-bound work takes about twice as long as on a whole core, and while it runs in the event loop, heartbeats wait.
What genuinely needs more is anything large that stays resident or spikes hard.
- A headless browser: a second large application running beside your bot.
- Image and audio processing, where memory follows the decoded media rather than the file size, and voice transcoding, which is continuous CPU work.
- A local model, whose weights stay resident and whose inference runs on the CPU. A hosted model API costs the bot almost no memory by comparison.
State: JSON files, SQLite or a database server
Anything the bot must remember across a restart has to be written somewhere that survives one. For a few settings per server a JSON file is fine if you write it safely: write the new version to a temporary file, then rename it over the old one. On Linux a rename replaces the target atomically, so a crash mid-write leaves the old file or the new one, never half of each.
Beyond a handful of records, SQLite is the natural next step: one file, no server. Its documentation is candid about the limits: any number of readers but one writer at a time, and a client/server database is the better choice when many writers compete or the data sits across a network from the application. Several bots or a dashboard on different machines writing the same data is the point to move.
Copying a SQLite file mid-transaction can produce a corrupt copy, so take backups with VACUUM INTO or the backup API, then move the copy off the machine.
- Code lives in git, data lives in backups, and the container is neither. A host is where the bot runs, not where its only copy lives.
- Restore a backup once, deliberately, before you need to.
Python and Node habits that bite in production
Pin dependencies. In Python, pin exact versions with == in requirements.txt; pip freeze writes such a file, including your dependencies' dependencies, and pip's hash-checking mode also verifies each download. In Node, commit package-lock.json and install with npm ci, which requires the lock file, errors instead of rewriting it when it disagrees with package.json, and removes any existing node_modules first.
Start the bot with one plain foreground command, such as node index.js or python bot.py, and read configuration from environment variables.
Log to stdout and stderr, which is what a supervisor or panel console captures. Python has a trap: when stdout is not an interactive terminal it is block-buffered, so print output can sit in a buffer and vanish when the process is killed. Run python -u, set PYTHONUNBUFFERED, or use the logging module, whose default handler writes to stderr, which Python line-buffers. discord.py's run() sets that logging up for you.
Never block the event loop. discord.py logs 'Heartbeat blocked for more than N seconds' when a handler does, and its FAQ says to use asyncio.sleep rather than time.sleep and aiohttp rather than requests. Long synchronous work in Node stalls heartbeats the same way.
Telegram: long polling or a webhook
Telegram offers two mutually exclusive ways to receive updates, and keeps undelivered ones for up to 24 hours either way.
Long polling means calling getUpdates, and Telegram holds the request open until something arrives. It is outbound only, so the host needs nothing but internet access. Set the timeout parameter: it defaults to 0, which is short polling, and the documentation says short polling is for testing only. Updates are confirmed by the offset you send next, so a bot that crashes mid-batch sees some of them again. Make handlers safe to run twice.
A webhook reverses the direction. Telegram posts each update to a public HTTPS URL you own, on port 443, 80, 88 or 8443, with a valid certificate; a self-signed one works only if uploaded through setWebhook. Set a secret_token and check the X-Telegram-Bot-Api-Secret-Token header on every request, or anyone who finds the URL can send fake updates. Reply with a 2xx promptly and do slow work afterwards, because failed deliveries are retried and eventually abandoned.
- getUpdates does not work while a webhook is set. When switching, drop_pending_updates discards the backlog instead of replaying a day of stale messages.
- Telegram's FAQ advises at most one message per second in a single chat, allows 20 messages a minute to a group, and caps broadcasts at about 30 a second unless you enable paid broadcasts. Beyond that come 429 errors.
Calling an AI model without hangs or surprise bills
An AI reply takes longer than Discord allows. An interaction needs its first response within 3 seconds or its token is invalidated, so defer at once, which shows a loading state, and edit the answer in when it is ready. The token stays valid for 15 minutes.
That window is why you set your own timeouts. Anthropic's Python SDK, to take one provider, times a request out after 10 minutes by default and retries a timed-out request twice, so one stuck call can outlive the interaction it was answering. Give chat replies a timeout in seconds, and stream the long answers that need more.
Retry transient failures with exponential back-off and jitter, honouring retry-after when it is sent. Official SDKs already retry connection errors, rate limits and server errors twice, so your own retry loop multiplies the attempts; let one layer own retries. Not every 429 deserves one: Anthropic's monthly spend cap returns a 429 without retry-after, and it keeps failing until access resumes.
Control cost by design: set max_tokens, trim the history you send, add per-user cooldowns, and set a spend limit with the provider. Keep the key in the bot's environment, never in a message or a log line.
- Use an async client in discord.py. A blocking HTTP call stalls heartbeats for as long as the model takes.
- Cap concurrency with a small semaphore. Short bursts can trip per-minute limits even when the average is well below them.
- Discord caps message content at 2,000 characters, so split long output or attach it as a file.
Where to run it, and what Puranode's plans are
If the bot serves your own server and nobody minds an evening of downtime, run it on a machine you already leave on, under systemd or Docker as above, and spend nothing. Paying makes sense once other people rely on the bot, or when you would rather not own the machine, the reboots and the network.
Puranode's bot plans: Hobby is €4.99 a month for one bot on 0.5 vCPU and 512 MB of RAM, with auto-restart on crash and live console logs. Standard is €9.99 for up to three bots on 1 vCPU and 1 GB, with secrets set as environment variables. Scale is €24.99 for up to ten bots on 2 vCPU and 4 GB, currently set up on request because of node capacity. Prices include VAT.
The limits, plainly. There is one region: Helsinki, Finland. The 99.9% on the Standard plan is an uptime target, not an SLA, and there are no service credits. No plan has a GPU, so these plans suit bots that call a hosted model rather than run one.