First, check whether the process is still running
Everything else follows from this one question. If the process is gone, the bot was stopped from outside or crashed, and the log ends at the moment it died. If the process is still running but the bot shows offline, the connection to Discord's gateway closed and the library is either still reconnecting or has given up.
A bot started from a terminal belongs to that terminal. Close the window or lose the SSH session and the bot stops with it. A laptop that sleeps suspends the process and its network connection, and an operating system update reboots the machine with nothing to start the bot again. If the bot drops when you shut your laptop and returns when you open it, that is the whole story: it needs a machine that stays on.
Crashes are the other way a process disappears, and the two libraries behave differently. In discord.py, an exception raised inside an event handler is logged and ignored by default, so a bad command rarely kills the bot; the process ends when run() itself raises, for example on a bad token or a missing intent. In Node, an unhandled promise rejection becomes an uncaught exception by default since Node 15, and an 'error' event emitted with no listener throws and exits the process. One forgotten await in a discord.js handler can take the whole bot down.
- Process gone, log ends in a stack trace: a crash. Fix the error, then add a supervisor
- Process gone, no error at all: something stopped it from outside, such as a closed terminal, sleep, a reboot or the out-of-memory killer
- Process running, bot offline: a gateway problem. Read the sections on reconnects, intents and tokens
- In discord.js, register client.on('error', ...) and process.on('unhandledRejection', ...) so errors are logged instead of ending the process
Nothing restarts it: systemd, PM2 and Docker
Even a well-written bot will exit eventually, so something other than you has to start it again. On Linux that is systemd with Restart=on-failure on a unit enabled at boot. For Node bots, PM2 restarts an app automatically when it exits or crashes, and pm2 startup with pm2 save brings it back after a reboot. In Docker, a restart policy of unless-stopped or always does the same, with one catch: Docker only applies the policy once a container has stayed up for at least 10 seconds, so a bot that dies straight after starting is not retried.
Restart with a growing delay, not instantly. Every fresh start identifies with the gateway, and Discord allows 1,000 identifies in 24 hours. Past that, Discord terminates every session, resets the bot token and emails the owner. A bot that crash-loops right after logging in can spend that allowance in a day, and then it is offline until you put in a new token.
- systemd: RestartSec=10s with RestartSteps=4 and RestartMaxDelaySec=160s restarts after 10, 20, 40, 80, then 160 seconds
- PM2: --exp-backoff-restart-delay=100 grows the delay after each crash, caps it at 15 seconds, and resets it once the app stays up for 30 seconds
- Docker: --restart unless-stopped, and a manually stopped container stays stopped
- Test it: kill the process by hand, reboot once, and confirm the bot comes back both times
Gateway drops and reconnects
A bot holds an outbound WebSocket to Discord's gateway and sends heartbeats at an interval Discord sets. If an acknowledgement does not arrive before the next heartbeat, Discord's documentation calls the connection zombied: the client closes it, reconnects and resumes the session. Short gaps like this are normal and the libraries handle them. discord.py's run() reconnects by default and fires on_disconnect and on_resumed as it goes. discord.js reconnects automatically and reports it through the shardReconnecting and shardResume events.
The gaps get long when the bot cannot send its heartbeat on time. A handler that blocks the event loop, such as a synchronous HTTP request or a long computation, stalls heartbeats for as long as it runs. discord.py warns about this in its log with a line saying the shard's heartbeat blocked for more than a number of seconds. The fix is to keep blocking work off the event loop.
Some closes are final. Discord marks close codes 4004, 4010, 4011, 4012, 4013 and 4014 as not reconnectable: authentication failed, an invalid shard, sharding required, an invalid API version, invalid intents and disallowed intents. discord.js then emits shardDisconnect, which its source describes as a disconnect that will no longer reconnect. A restart does not help with any of these; the cause has to be fixed first.
- Log shardReconnecting, shardResume and shardDisconnect in discord.js, or on_disconnect and on_resumed in discord.py, so you can see how often it happens
- Gateway sends are limited to 120 events per connection every 60 seconds, and a bot over that is disconnected immediately. Loops that change presence are the usual cause
- A bot in 2,500 or more servers must shard; without it Discord closes the connection with 4011
Missing privileged intents
Three intents are privileged: Server Members, Presence and Message Content. Each must be switched on in the Developer Portal, on your application's Bot page, before the bot may request it. Request one that is not enabled and Discord closes the connection with code 4014. discord.py raises PrivilegedIntentsRequired and discord.js reports Used disallowed intents. The bot never gets online, and restarting it changes nothing until the switch in the portal is on, or the intent is removed from your code.
Apps with fewer than 10,000 users can enable privileged intents themselves in the portal. Past 10,000 unique users, continued access requires a review, and Discord notifies the owner when that happens.
There is also a version of this that looks like being offline. A bot without Message Content stays connected, but the content, embeds and attachments of other people's messages arrive empty, so prefix commands and keyword triggers simply do nothing. The exceptions are the bot's own messages, 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.
Rate limits, 429s and the Cloudflare block
Discord allows a bot 50 requests per second to its API overall, plus per-route limits. Exceed one and the API answers 429 with a JSON body whose retry_after says how many seconds to wait. Both libraries queue and wait on their own: discord.py logs a warning starting We are being rate limited and retries, and discord.js waits and emits a rateLimited event on client.rest. Occasional 429s are therefore not an outage, but a bot that is constantly rate limited feels frozen.
The serious case is the invalid request limit. Discord counts every response with status 401, 403 or 429, and an IP that makes 10,000 of them in 10 minutes is temporarily blocked by Cloudflare. Discord says such a block is most likely not a global rate limit issue but a spike of unhandled errors: a loop retrying a request it has no permission for, or a bot still using a revoked token. While the block lasts, every request from that address fails.
- Never retry a 401 or 403 in a loop. Fix the permission or the token instead
- Honour retry_after and do not hard-code limits; Discord's headers say what applies
- 429s with X-RateLimit-Scope: shared do not count towards the invalid request limit
- discord.js emits invalidRequestWarning on client.rest when you set invalidRequestWarningInterval, an early warning before a block
Invalid or reset tokens
A bot with a bad token fails at login, every time, and no amount of restarting helps. discord.py raises LoginFailure with Improper token has been passed. discord.js throws An invalid token was provided. On the gateway the same problem is close code 4004, authentication failed.
There are three common causes. You reset the token in the Developer Portal and the bot still has the old one. Discord reset it for you after the bot hit the 1,000 identify limit, in which case the owner received an email. Or the value is not a bot token at all: the discord.js guide notes that pasting the application's client secret is a frequent mistake.
The fix is the same each time. Open the Bot page in the Developer Portal, use Reset Token, and copy the new value straight into the bot's environment variables, because Discord shows a token only once. Never commit it to version control: if it ever reached a public repository, treat it as leaked and reset it before doing anything else.
Hosts that sleep idle apps, and memory that runs out
Some free hosting platforms suspend an app that receives no incoming web traffic for a while. A Discord bot receives no web traffic at all, because it talks to Discord over its own outbound connection, so on such a platform it is idle by definition and goes to sleep. The signature is regular: the bot drops after a quiet spell and returns only when something wakes the app. That is the platform's policy working as intended, and the fix is a host that runs processes continuously.
Memory is the slower failure. When a process exceeds its limit, the kernel kills it: inside a Docker container with --memory set, the kernel kills processes in the container, and on a bare machine the out-of-memory killer picks a victim. The bot vanishes with no traceback of its own, often after hours or days, and memory graphs show a steady climb beforehand. Node bots also have V8's own heap limit, which --max-old-space-size sets; as usage nears it, V8 spends more and more time on garbage collection, so a leaking bot slows down before it dies.
The usual cause in a bot is caches that grow with every server, member and message it sees. discord.py keeps the last 1,000 messages by default through max_messages. discord.js lets you cap caches with makeCache and prune them with sweepers. Request only the intents you need, since each one is more data to receive and often to cache.
- Measure memory after a week of normal use, not a minute after login
- PM2's max_memory_restart restarts a bot past a threshold, checked every 30 seconds: a safety net while you find the leak, not a fix
- Set the container or service memory limit above what you measured, so a real leak shows up as a pattern rather than random deaths