When a Discord bot is not working, it fails in one of two ways. Either it shows as offline, because the process stopped, the token changed or Discord refused the connection, or it is online but not responding, because it can't read messages, its commands aren't registered or it answers too slowly. The causes don't overlap, so find your symptom below and jump to the fix.
Everything here was checked against Discord's developer docs in September 2026. The code was tested with discord.js 14.27.0 on Node 24.15.0 and discord.py 2.7.1 on Python 3.14.3, against a local stand-in for Discord's API that enforces the documented rules: a rejected token, close code 4014 and the 3-second response window.
Find why your Discord bot is not working
| What you see | Most likely cause | Section |
|---|---|---|
| The bot shows as offline | The process isn't running, the host went to sleep or the token changed | Offline |
An invalid token was provided. or Improper token has been passed. |
The token was reset, or your .env file isn't being read |
Token |
Used disallowed intents, PrivilegedIntentsRequired or close code 4014 |
Your code asks for a privileged intent the portal doesn't allow | Intents |
Online, but ! commands get no answer |
The Message Content intent is off or missing from your code | Message Content |
| Slash commands don't show up | Not registered, registered in another server, or hidden by permissions | Slash commands |
| "The application did not respond" or "This interaction failed" | No response within 3 seconds | 3-second rule |
| Every command runs twice | Two copies of the bot are running | Duplicates |
Missing Permissions (error 50013) |
A role, channel or hierarchy problem | Permissions |
ReferenceError: ReadableStream is not defined on start |
Node.js is too old for discord.js 14 | Old versions |
| 429 errors, or "You are being blocked from accessing our API temporarily" | Rate limits | Rate limits |
| The bot can't join more than 100 servers | The app isn't verified | Verification |
Is it your bot or someone else's?
If it's a public bot such as MEE6, Carl-bot or an app from the App Directory, you can't change its code. Check Discord's status page for an API or gateway incident, the bot's own status page or support server, and whether the bot still has its permissions in that channel (the guide to adding bots covers this).
If the bot is yours, or someone built it on your Discord application, keep reading.
Read the error first
A broken bot usually prints an error that names the cause. Where it shows up depends on how the bot runs: the terminal you started it from, your host's console tab, pm2 logs under PM2, journalctl -u <service> under systemd, or docker logs <container> under Docker.
If the logs are empty or unclear, put this one-off check next to your .env file and run node check-bot.js. It explains the common connection failures in plain words.
// check-bot.js: put it in your bot's folder and run `node check-bot.js`.
// It logs in once, prints what Discord sees, then exits.
// Check Node before loading discord.js, which crashes on load under Node 16.
const major = Number(process.versions.node.split('.')[0]);
if (major < 18) {
console.error(`Node ${process.version} is too old for discord.js 14. Install Node 22 or 24.`);
process.exit(1);
}
// Read .env from the folder you run this in (Node 20.12 and newer).
try {
process.loadEnvFile();
} catch {
// No .env file or an older Node: DISCORD_TOKEN must already be set.
}
const { Client, Events, GatewayIntentBits, Routes } = require('discord.js');
// Paste the exact intents your bot uses, so this script fails the same way it does.
const INTENTS = [GatewayIntentBits.Guilds];
const CLOSE_CODES = {
4004: 'Token rejected. It was reset or copied wrong: use Reset Token on the Bot page.',
4011: 'Sharding required: the bot is in too many servers for one connection.',
4013: 'Invalid intents: the intents value in your code is malformed.',
4014: 'Disallowed intents: turn the privileged intent on under Bot > Privileged Gateway Intents, or remove it from your code.',
};
if (!process.env.DISCORD_TOKEN) {
console.error('DISCORD_TOKEN is empty. The .env file was not loaded, or the variable has another name.');
process.exit(1);
}
const client = new Client({ intents: INTENTS });
client.on(Events.ShardDisconnect, (event) => {
console.error(`Gateway closed with code ${event.code}. ${CLOSE_CODES[event.code] ?? 'Look the code up in Discord\'s close code table.'}`);
process.exit(1);
});
client.once(Events.ClientReady, async (c) => {
console.log(`Logged in as ${c.user.tag}, application ID ${c.application.id}`);
console.log(`Servers: ${c.guilds.cache.size}`);
const { session_start_limit: limit } = await c.rest.get(Routes.gatewayBot());
console.log(`Logins left before the daily limit: ${limit.remaining} of ${limit.total}`);
const global = await c.application.commands.fetch();
console.log(`Global commands: ${global.map((cmd) => `/${cmd.name}`).join(', ') || 'none'}`);
// Guild commands show up only in that one server. Check the first 10 servers.
for (const guild of [...c.guilds.cache.values()].slice(0, 10)) {
const local = await guild.commands.fetch().catch((error) => {
console.log(`Couldn't list commands in ${guild.name}: ${error.message}`);
return null;
});
if (local?.size) {
console.log(`Only in ${guild.name}: ${local.map((cmd) => `/${cmd.name}`).join(', ')}`);
}
}
await c.destroy();
});
client.login(process.env.DISCORD_TOKEN).catch((error) => {
console.error(`Login failed: ${error.message}`);
process.exit(1);
});A healthy bot prints something like this:
The command lines show whether slash commands are registered where you think. Few logins left usually means a restart loop, which ends in a token reset (see rate limits).
The bot is offline
An offline bot has no live connection to Discord's gateway: the process isn't running, the token no longer works, or Discord closed the connection over intents.
The process isn't running
If the process exited, the last lines of its log say why. Usually it's an unhandled error: one uncaught exception or rejected promise stops a Node process, a process manager restarts it, and it crashes again. The bot.js below logs errors instead of dying.
Free hosting is the other common reason. As of September 2026:
- Render spins down a free web service after 15 minutes without inbound traffic and takes about a minute to wake it. A bot gets no web visitors, so it sleeps. Background workers, which suit a bot better, can't use Render's free instances.
- BotGhost says bots on its free plan "may go offline after 48 hours of inactivity."
- Heroku began shutting down free dynos on November 28, 2022, and Glitch stopped hosting projects on July 8, 2025. A bot that lived on either won't come back there.
- Replit's docs use a Reserved VM deployment as their example of an always-online Discord bot.
Paid hosting for a small bot costs a few dollars a month: PebbleHost's 1 GB plan is $3, and Railway's Hobby plan is $5 including $5 of usage. The 24/7 hosting guide compares the options.
A bot can also stay green for a few minutes after it dies: Discord's docs say a session that ends without a clean close "will remain active and timeout after a few minutes."
The token is invalid or was reset
discord.js says An invalid token was provided. and discord.py says Improper token has been passed. On the gateway, a rejected token is close code 4004, "Authentication failed." The usual causes:
- The variable is empty. discord.js throws the same invalid-token error when
process.env.DISCORD_TOKENis undefined, before it even contacts Discord. The.envfile wasn't loaded, sits in another folder, or the host uses a different variable name.check-bot.jsreports this case separately. - Someone pressed Reset Token. The old token stops working everywhere at once.
- It leaked. Discord is a GitHub secret scanning partner, so a bot token pushed to a public repository or gist is reported to Discord and reset. Tokens posted in public Discord channels are sometimes uploaded to a gist by others on purpose, to trigger the same reset.
- The bot logged in too often. Discord allows 1,000 logins (IDENTIFY calls) per 24 hours. Its docs say that on hitting the limit, "all active sessions for the app will be terminated, the bot token will be reset, and the owner will receive an email notification." A bot that crashes and restarts every minute makes 1,440 attempts a day.
- It's the wrong value. The Client Secret on the OAuth2 page and the Public Key on General Information are not the bot token. The token comes only from Reset Token on the Bot page.
To fix it, reset the token on the Bot page, put the new one in your host's environment variables rather than in code, and restart. Add .env to .gitignore. If a crash loop caused the reset, fix the crash first. The Discord bot token guide covers storing the new token safely and what to do after a leak.
"Used disallowed intents" (close code 4014)
Your code asks for a privileged intent that isn't switched on for your app. Discord closes the connection with code 4014, discord.js crashes with Error: Used disallowed intents and discord.py raises PrivilegedIntentsRequired. The bot never comes online.
Three intents are privileged: Server Members (GuildMembers), Presence (GuildPresences) and Message Content (MessageContent). What you do depends on your app's size, under rules Discord changed on June 10, 2026:
- Fewer than 10,000 users. Open your app in the Developer Portal, go to Bot > Privileged Gateway Intents, switch on exactly what your code asks for and restart the bot. A running bot only picks up the change on a new connection.
- 10,000 users or more. The toggles need an approved request. Discord counts "unique users who have access to your app across all the servers it's installed in," notifies the owner by email or system DM when the app crosses the line and gives 90 days to apply, after which unapproved access is removed. The app keeps working while its request is reviewed, and approved apps reapply once a year.
Before June 10, 2026, the line was 100 servers, so older tutorials that say "apply once you pass 100 servers" are out of date.
If the bot doesn't need the data, take the intent out of your code instead. Discord's own review guide calls text commands "the most common reason developers request the Message Content privileged intent" and points to slash commands, which need no privileged intent at all.
Online but ignoring messages
If the bot is online but !help and every other prefix command get no answer, it is probably receiving messages with the text removed. Message Content became a privileged intent on September 1, 2022. Without it, the content, embeds, attachments, components and poll fields arrive empty, except in DMs with the bot, in messages that mention it, in its own messages and in messages targeted by a message context menu command. That's why @YourBot help often works when !help doesn't.
A prefix-command bot needs the intent in two places: switched on in the portal, and requested in code, together with the intents that deliver messages in the first place. In discord.js 14:
const { Client, GatewayIntentBits, Partials } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, // messageCreate in servers
GatewayIntentBits.MessageContent, // privileged: the text of those messages
GatewayIntentBits.DirectMessages, // messageCreate in DMs
],
partials: [Partials.Channel], // DM channels aren't cached, so DM messages need this
});The DM line is easy to miss. discord.js doesn't cache DM channels, so without the Channel partial a DM never reaches messageCreate. In my test the DM arrived with the partial and silently disappeared without it.
In discord.py, set intents.message_content = True before creating the bot; the full discord.py file is further down. Without it, discord.py logs Privileged message content intent is missing, commands may not work as expected.
For anything new, use slash commands. They need no privileged intent and members can see what exists.
Slash commands don't show up
A slash command exists only after your code registers it with Discord, and being online says nothing about that. Run check-bot.js, compare its command lists with what you expect, then work down this list:
- They were never registered. Most discord.js projects have a separate deploy script. It has to run again whenever a command's name, description or options change. Changes inside a handler need no redeploy.
- They were registered to one server. Guild commands appear only in the server they were registered to, usually a test server. Register them globally for everyone else.
- They show up twice. The same commands are registered both globally and in a guild. Delete the guild copies.
- Another deploy wiped them. Registering a list replaces the app's whole list, and Discord's docs warn that the bulk endpoint overwrites "all types of application commands." Two projects that share one application and both deploy this way, such as a test bot and the live one, keep deleting each other's commands. Give each its own application.
- Members can't use them. Server Settings > Integrations can limit each command to certain roles and channels, and members need Use Application Commands in the channel. The bot permissions guide covers both.
- The Discord app hasn't caught up. Discord's docs say guild commands update instantly and that an outdated global command is reloaded when someone tries to use it. If a new command still isn't listed, restart the Discord app before you debug further.
Discord also caps command creation at 200 new commands per day per server, so register from a deploy script, never in a loop. The slash commands guide has a tested deploy script.
"The application did not respond"
That message on a slash command, or "This interaction failed" on a button or menu, means Discord got no answer from the bot in time. Discord's docs are strict: you "must send an initial response within 3 seconds of receiving the event." Once you have responded, the interaction token stays valid for 15 minutes for edits and follow-ups.
Anything slow before the first response breaks the rule: a database query, an outside API, an AI model, a slow host. The fix is to defer: acknowledge right away, then edit the reply when the work is done.
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('report')
.setDescription('Build this week\'s activity report'),
async execute(interaction) {
// Acknowledge first. Members see "<bot> is thinking..." while the work runs.
await interaction.deferReply();
// You now have 15 minutes instead of 3 seconds.
const report = await buildReport(interaction.guildId);
await interaction.editReply(report);
},
};
// Stand-in for a slow database query or API call.
async function buildReport(guildId) {
await new Promise((resolve) => setTimeout(resolve, 5000));
return `Weekly report for server ${guildId} is ready.`;
}The failing version of execute() does the slow work first:
With the same 5-second task, that version fails with DiscordAPIError[10062]: Unknown interaction, while the deferred one posts the report. Three errors look alike and mean different things:
| Error | What happened | Fix |
|---|---|---|
DiscordAPIError[10062]: Unknown interaction |
The first response came after the 3 seconds | Defer first, then do the work |
DiscordAPIError[40060]: Interaction has already been acknowledged. |
Something else answered first, usually a second copy of the bot | Stop the other copy |
The reply to this interaction has already been sent or deferred. |
Your own code responded twice | Use editReply() or followUp() after the first response |
The other cause is a handler that throws before it replies: the member sees "did not respond" and you see nothing. This main file logs every failure and still answers, using whichever response Discord allows at that point:
const { Client, Events, GatewayIntentBits, MessageFlags } = require('discord.js');
const client = new Client({
// Slash commands need only Guilds. Prefix commands also need
// GuildMessages and the privileged MessageContent intent.
intents: [GatewayIntentBits.Guilds],
});
const commands = new Map([['report', require('./commands/report')]]);
client.once(Events.ClientReady, (c) => {
console.log(`Online as ${c.user.tag} in ${c.guilds.cache.size} servers`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = commands.get(interaction.commandName);
if (!command) {
console.warn(`No handler for /${interaction.commandName}. An old command may still be registered.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(`/${interaction.commandName} failed:`, error);
const content = 'That command failed. The error has been logged.';
// Replying twice throws, so use whichever response is still allowed.
if (interaction.replied) {
await interaction.followUp({ content, flags: MessageFlags.Ephemeral }).catch(console.error);
} else if (interaction.deferred) {
await interaction.editReply({ content }).catch(console.error);
} else {
await interaction.reply({ content, flags: MessageFlags.Ephemeral }).catch(console.error);
}
}
});
// Log errors instead of crashing, so a process manager doesn't restart the bot in a loop.
client.on(Events.Error, (error) => console.error('Client error:', error));
process.on('unhandledRejection', (error) => console.error('Unhandled rejection:', error));
client.login(process.env.DISCORD_TOKEN);Run it with node --env-file=.env bot.js on Node 20.6 or newer, or load the file with the dotenv package. I tested every branch: an error before any reply, after the defer and after the edit, plus a second copy of the bot answering the same interaction.
The bot answers twice
If every command runs twice, or error 40060 shows up while the bot seems to work, two copies are logged in with the same token, and both act on every event. Usually the bot still runs on the old host after a move, a test copy runs on a laptop, or a process manager started a second instance.
Stop the extra copy. If you can't find it, reset the token. Every copy using the old token loses access, and only the one you update comes back.
Missing Permissions (error 50013)
Error 50013, "You lack permissions to perform that action," means the bot tried something its role or the channel doesn't allow. Usually a permission was never granted, a channel overwrite denies View Channel, the server requires 2FA for moderation and the bot owner's account doesn't have it, or the role hierarchy blocks it: a bot can only manage roles below its own highest role. The guide to adding bots goes through each one with a tested helper that explains the failure before it happens, and the permissions calculator builds a fresh invite link.
Two recent Discord changes broke bots that had worked for years:
- February 23, 2026. Pinning messages needs the Pin Messages permission, and Manage Messages is no longer enough. Creating emoji and stickers needs Create Expressions, and creating scheduled events needs Create Events.
- November 16, 2026. Bots stop receiving the details of channels they can't view, and the channel list leaves those channels out. A bot that looks up a channel by name will need View Channel there first.
Rate limits and 429 errors
Every bot gets 50 API requests per second in total, individual routes have their own limits, and a request over a limit gets HTTP 429 with a retry_after value. discord.js and discord.py both wait and retry by themselves (discord.py logs We are being rate limited), so an occasional 429 is normal.
Constant 429s mean a loop, such as a status message edited every second or a DM to every member at once. To see which route is involved, log discord.js's rate-limit event:
Two limits do more damage than a slowdown:
- Invalid requests. An IP address that makes 10,000 invalid requests (responses with status 401, 403 or 429) in 10 minutes is temporarily blocked from the API. A request that fails with 403 and is retried in a loop gets there fast. The error reads "You are being blocked from accessing our API temporarily due to exceeding our rate limits frequently." The limit counts per IP address, so on a shared host other bots on the same address count toward it.
- Logins. 1,000 IDENTIFY calls per 24 hours, and hitting it resets your token, as described above. A process manager restarting a crashing bot every few seconds burns through it in hours.
Node.js or the library is too old
discord.js 14.27.0, the current release, needs Node.js 18 or newer, and has since version 14.16.0 in September 2024. Node 18 reached end of life on April 30, 2025 and Node 20 on April 30, 2026, so run Node 22 or 24. On Node 16, discord.js 14 crashes the moment it loads:
Run node --version on the host itself, not only on your computer, because the two can differ.
Code written for discord.js v12 or v13 breaks the other way round when a fresh install pulls v14 under it. These are the errors I got running old code on 14.27.0:
| Error on discord.js 14 | Old code | Replace with |
|---|---|---|
TypeError: Cannot read properties of undefined (reading 'FLAGS') |
Intents.FLAGS.GUILDS |
GatewayIntentBits.Guilds |
TypeError: Discord.MessageEmbed is not a constructor |
new Discord.MessageEmbed() |
new EmbedBuilder() |
Invalid bitfield flag or number: GUILDS. |
intents: ['GUILDS'] |
intents: [GatewayIntentBits.Guilds] |
Valid intents must be provided for the Client. |
new Client() with no options |
Pass an intents list |
The discord.js v14 update guide lists every rename. Two deprecation warnings on current v14 are worth clearing before v15: since 14.22.0 (August 2025) the ready event is called clientReady, and ephemeral: true has been replaced by flags: MessageFlags.Ephemeral.
On the Python side, discord.py 2.7.1 needs Python 3.8 or newer. Code written for discord.py 1.x fails on 2.x mostly because intents is now required (missing 1 required keyword-only argument: 'intents'), extensions and cogs load asynchronously, and user-account features are gone. The official migration guide covers the rest.
Very old bots also use old API versions: discord.js v12 and discord.py 1.7.3 speak API v7 and gateway v6, which Discord's docs list as deprecated but not yet discontinued. Even where they still run, they have no slash commands, buttons or threads, and get no fixes when Discord changes something.
Stuck at 100 servers
An unverified app can't grow past 100 servers. Discord's help center puts it plainly: "Verification is required for your app to scale past 100 servers." The owner starts it from the App Verification tab of the app in the Developer Portal, and the owner of the team that owns the app has to verify their identity through Stripe. Since June 10, 2026, verification and privileged intent review are separate processes; intent access follows the 10,000-user rule above.
The same fixes in discord.py
This file covers both fixes for discord.py: the Message Content intent for a !ping prefix command, and a deferred slash command.
import asyncio
import os
import discord
from discord.ext import commands
intents = discord.Intents.default()
intents.message_content = True # privileged: switch it on in the Developer Portal too
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.command()
async def ping(ctx: commands.Context):
await ctx.send("pong")
@bot.tree.command(name="report", description="Build this week's activity report")
async def report(interaction: discord.Interaction):
# Acknowledge within 3 seconds. Members see "<bot> is thinking..."
await interaction.response.defer(thinking=True)
text = await build_report(interaction.guild_id) # you now have 15 minutes
await interaction.edit_original_response(content=text)
async def build_report(guild_id):
await asyncio.sleep(5) # stand-in for a slow query or API call
return f"Weekly report for server {guild_id} is ready."
bot.run(os.environ["DISCORD_TOKEN"])Register the slash command once with await bot.tree.sync(); the discord.py guide shows where to call it. With Message Content switched off in the portal, this file stops at login with PrivilegedIntentsRequired. With it on, both !ping and /report answered in my test.
When to hand it over
If your problem matches a row in the first table and the fix is a toggle, a token or a line of config, you can do it yourself in a few minutes. It's a different job when the code is still on discord.js v12 or v13 or discord.py 1.x, the developer who wrote it has disappeared, or the host it lived on no longer exists.
That's what Bot Rescue is for: one discord.js or discord.py bot fixed within 3 business days for a fixed $149, with a short written report of what was wrong. If I can't fix it, you pay nothing, and you never have to send me your token. After that, Care keeps the bot hosted and updated from $49 a month.