A Discord bot is online exactly as long as its process is running. There is no queue that wakes it up and no request that starts it on demand: if the process is not connected to the gateway, the bot shows as offline and every slash command fails with "The application did not respond". Hosting a bot 24/7 therefore means one unglamorous thing - keeping a small Node process alive on a machine that is not your laptop, restarting it when it dies, and redeploying it when you change it.
This is the whole path for a discord.js bot: the application and its token, the intents you declare, a project layout that starts cleanly on a server, slash-command registration, deploying from GitHub, and the handful of failure modes that take bots offline at three in the morning. Sizing is a separate question, answered in how much RAM and CPU a Discord bot needs; the short version is that a command bot in a few dozen servers fits comfortably in the smallest plan anyone sells.
What running 24 hours a day actually requires#
Your bot opens one outbound WebSocket to the Discord gateway and keeps it open. Discord sends a hello payload containing a heartbeat_interval (typically just over forty seconds), and the library sends a heartbeat on that interval for as long as the connection lives. Events arrive on that socket; anything the bot does back - replying, editing, registering commands - goes over the REST API as ordinary HTTPS requests.
Two consequences follow, and they shape everything about hosting a bot.
- Nothing needs to reach your bot from outside. No inbound port, no domain, no certificate, no firewall rule. A bot works perfectly from behind a NAT. If you later add a web dashboard, that is a second, different service with its own requirements.
- The process must be resident. Platforms that sleep an app after a period with no HTTP traffic are not suitable, because a bot receives no HTTP traffic at all. Any arrangement where the process is stopped and started per request is the wrong shape for this workload.
| What the bot needs | What it does not need |
|---|---|
| A process that stays running | An inbound port |
| Outbound HTTPS and WebSocket | A domain or certificate |
| A restart when it exits | A load balancer |
| Somewhere to store state | More than one process, below 2,500 guilds |
The other thing worth knowing before you deploy is that reconnecting is not free. When the socket drops, the library first tries to resume: it sends the session ID and the last sequence number it saw, and Discord replays the events that were missed. If the session cannot be resumed, the bot must identify again, and identifies are rate limited - most bots get 1,000 per day. You can read your own allowance with an authenticated GET /gateway/bot, which returns a session_start_limit object with total, remaining and reset_after. A bot in a crash loop burns through that allowance, which is why a restart loop is a real problem and not just noise in the log.
The application, the token and the invite#
Everything starts in the Discord Developer Portal. Create an application, open its Bot tab, and reset the token. It is shown once. If you lose it, reset it again, which invalidates the old one immediately and takes the bot offline until you deploy the new value.
A bot token is three base64-ish parts separated by dots, and the first part is your application ID encoded, so a leaked token is not anonymous: anyone who sees it knows which bot it belongs to. Discord participates in GitHub's secret scanning, so a token pushed to a public repository is usually invalidated within minutes. That is the good outcome. The bad one is the token in a private repository that later becomes public. Treat it as a password that grants full control of the bot, and keep it in an environment variable - where to put secrets on an application server covers the general case.
Two switches on that page catch people out:
- Public bot decides whether anyone can invite your bot. Turn it off for a private bot and only you can add it to servers.
- Requires OAuth2 code grant should be off. It is for a flow almost nobody is running, and with it on, the normal invite link fails with a confusing error.
Build the invite under OAuth2, URL Generator. Tick bot and applications.commands as scopes - a bot invited without applications.commands will connect, appear online, and have no slash commands, which is one of the most common "my commands do not show up" causes. Then pick the permissions the bot actually needs. Permissions can be added later only by re-inviting with a new link or by editing the bot's role in the server, so it is worth thinking about once rather than asking every server owner to do it twice.
Intents, and what you are actually subscribing to#
Intents are declared when you connect, and they decide which events Discord sends you at all. Getting them wrong produces silence rather than an error, so it is worth being deliberate.
import { Client, GatewayIntentBits, Partials } from "discord.js";const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, ], partials: [Partials.Message, Partials.Channel, Partials.Reaction],});Guilds is effectively mandatory: without it the library has no guild, channel or role cache and most of the API surface stops working. Three intents are privileged and must also be enabled on the Bot tab in the portal: GUILD_MEMBERS, GUILD_PRESENCES and MESSAGE_CONTENT. Once a bot is in more than 100 servers it has to be verified to keep them, which is a process with a waiting period, so decide early whether you need them. A bot built entirely on slash commands does not need MESSAGE_CONTENT at all, because the command payload arrives with the interaction.
The partials array is the other half. If an event refers to something the library has not cached - a reaction on a message posted before the bot started, for instance - discord.js will not emit the event at all unless you have opted into partial structures. With partials enabled you receive a stub and call .fetch() on it when you need the full object.
A project that starts itself#
On a server, nobody types the start command. It has to work from a cold container, in a directory that was just pulled from git, with no interactive shell.
{ "name": "my-bot", "type": "module", "engines": { "node": ">=20" }, "scripts": { "start": "node src/index.js", "register": "node scripts/register-commands.js" }, "dependencies": { "discord.js": "^14.16.3" }}The start command on the host should install from the lockfile and then run:
npm ci --omit=dev && node src/index.jsnpm ci deletes node_modules and installs exactly what the lockfile says, failing loudly if the lockfile and the manifest disagree. npm install quietly resolves something new, which is how a bot that worked on your machine picks up a broken minor version on the server. Commit package-lock.json. The difference is worked through in npm ci vs npm install.
Two version traps are worth naming. First, --omit=dev removes devDependencies, so a TypeScript bot cannot compile on the server with that flag: either build in CI and ship dist, or drop --omit=dev and accept the install size. Second, discord.js has raised its minimum Node version over the life of v14, and engines is a note to humans rather than something the container enforces. Print the runtime version at boot instead of assuming:
import { Events } from "discord.js";client.once(Events.ClientReady, () => { console.log(`node ${process.version}, logged in as ${client.user.tag}`); console.log(`guilds: ${client.guilds.cache.size}`);});Those two lines answer three support questions before they are asked: which Node is running, whether the token worked, and how many servers the bot can see. Use the Events constant rather than a string literal, because the underlying event names have changed across releases and the constant follows the version you installed.
One more environment detail: server containers run in UTC unless told otherwise. If your bot posts a daily message at 09:00, it will post at 09:00 UTC. Set a TZ environment variable, or do the arithmetic explicitly in code. Silent timezone drift is the most annoying bug in this category because it is only wrong by a fixed number of hours.
Registering slash commands without breaking anything#
Commands are registered through the REST API, not through the gateway, and they persist on Discord's side. That means registration is a deployment step, not a startup step.
import { REST, Routes } from "discord.js";const commands = [ { name: "ping", description: "Check that the bot is alive" },];const rest = new REST().setToken(process.env.DISCORD_TOKEN);await rest.put( Routes.applicationGuildCommands(process.env.APP_ID, process.env.GUILD_ID), { body: commands },);applicationGuildCommands registers into one server and takes effect immediately, which is what you want while developing. applicationCommands registers globally; Discord documents global commands as taking up to an hour to propagate, so a command that is missing right after a global registration is usually just early. A PUT replaces the whole set, so anything absent from the array is deleted - that is how you remove an old command, and also how people accidentally wipe their command list by registering a partial array.
Do not run registration on every start. Discord caps how many commands you can create per day, and a bot that registers on boot while stuck in a restart loop can lock itself out for the rest of the day. Run it as a separate command when the command definitions change, which for most bots is rarely.
If registration fails with Invalid Form Body and error code 50035, read the field path in the message. Command names must be lowercase, 1-32 characters, with no spaces; descriptions are 1-100 characters and required for chat input commands. The error is precise once you know it is pointing at a specific option.
Deploying from GitHub#
Uploading a zip file is how you end up with a server whose contents nobody can identify. Connect the repository instead.
On RE:NODE the Git integration is GitHub only, through a GitHub App that mints short-lived tokens rather than asking you to paste a personal access token that never expires, so private repositories work without a credential sitting on the server forever. There are two independent switches: pull the branch every time the container starts, and deploy on push, which restarts the server when GitHub reports a push to that branch - and only if it was already running, so a server you deliberately stopped stays stopped. Each deploy is recorded as one row, opened when the push arrives and closed when the container is seen running again, which is how you tell a deploy that shipped from one that fell over. The walkthrough is in deploy a Node.js app from GitHub.
A deploy costs a short gap: the process stops, the pull and install run, and the bot reconnects, typically in five to twenty seconds. During that window any interaction sent to the bot fails. With one process there is no way around it, so deploy when your users are asleep and keep the install fast - zero-downtime deploys on a server that only has one of everything is honest about which techniques apply to a gateway bot.
If the bot matters, give it a staging twin: a second application in the Developer Portal with its own token, invited only to your test server. Staging and production on one account has the pattern.
Secrets, state and the files your bot writes#
Environment variables go on the Startup tab, not in the repository. DISCORD_TOKEN, the application ID, any API keys, the database URL. Add .env to .gitignore on day one, and read the values with a hard failure if they are missing:
const token = process.env.DISCORD_TOKEN;if (!token) throw new Error("DISCORD_TOKEN is not set");Failing at boot with a clear message is much better than connecting with undefined and reading An invalid token was provided in the console.
State is the other half. Anything the bot writes inside its own repository directory is in the way of the next pull, so keep runtime data somewhere the deploy does not touch: a directory outside the tracked tree, or a database. A SQLite file is the right answer for a surprising number of bots - one file, no server, and it handles a bot's write volume without noticing. Move to a networked database when more than one process writes, or when the data stops fitting comfortably in memory. App plans include two database slots created in the panel with a generated host, user and password, and there are standalone lines on database hosting when the store outgrows the bot; connection pools and limits covers the mistake most bots make first, which is opening a connection per command.
Whatever you use, back it up on a schedule rather than when you remember. Backup slots come with every app plan and the Schedules tab runs them on a cron expression. A backup nobody has restored is a hypothesis: backups that actually restore makes that case properly.
Staying online: crashes, loops and the memory limit#
Three things take a running bot down, and all three are preventable.
An unhandled rejection ends the process. In current Node an unhandled promise rejection terminates the process by default. One failed fetch in a command handler with no catch is enough. Install the handlers, and make the log line happen before anything exits:
process.on("unhandledRejection", (error) => { console.error("unhandled rejection:", error);});process.on("SIGTERM", async () => { await client.destroy(); process.exit(0);});Closing the client on SIGTERM matters more than it looks. A bot killed without closing its socket stays visible as online for up to a minute afterwards, so a ten-second restart looks to your users like a minute of the bot ignoring them. Graceful shutdown and health checks has the general pattern.
The memory limit is not a graceful event. At the container limit the kernel stops the process and it restarts clean rather than being left to swap. For a bot that is the right default, but it means an unbounded cache or a growing Map shows up as a mysterious restart every few hours rather than as a slow decline. Watch the memory graph in the console for a day after any change; why your Node app dies at 2 GB on a 4 GB plan explains the second ceiling, the V8 heap, which is the one a Node process usually hits first.
A restart loop gets noticed. On RE:NODE a watcher polls every two minutes for a server that went offline or whose uptime went backwards, ignoring restarts you asked for. Three unexpected restarts in an hour puts a warning on the server page and opens a ticket; six leads to suspension. That is not arbitrary: a bot restarting every twenty seconds hammers the gateway and burns the identify allowance described earlier. Why your server keeps restarting covers diagnosing the loop.
For monitoring, remember that a bot has no endpoint to poll. Either read the console, or bind a tiny HTTP server on the port allocation that comes with the plan and point an uptime monitor at it through the proxy slot. Monitoring that tells you something is about keeping that list short.
Troubleshooting#
The bot is online but has no commands. Either the invite was missing the applications.commands scope, or the commands were registered globally and have not propagated. Register to your test guild to check in seconds.
"The application did not respond". Your handler did not answer within three seconds. Either it threw, or it is doing real work. Call interaction.deferReply() first, which extends the window to fifteen minutes, then editReply() when you have an answer.
Close code 4004 in a loop. Bad token. Usually a reset token that was never redeployed, or a variable name typo, or the client secret pasted instead of the bot token.
Close code 4014 in a loop. A privileged intent that is not enabled in the Developer Portal.
`Missing Permissions` (50013) or `Missing Access` (50001). Role hierarchy, most often. A bot cannot moderate a member whose highest role is above the bot's own, regardless of permissions, and cannot post in a channel where a channel override denies it.
HTTP 429, then nothing. You are rate limited; the response carries retry_after and the library waits for it. Sustained floods of rejected requests are worse than slow ones, because Discord's edge blocks addresses that make many invalid requests in a short window. Fix the loop rather than retrying harder - rate limits and abuse has the general shape.
FAQ#
Can I host a Discord bot for free?
You can run one on a spare computer at home, and it will be offline whenever that machine is asleep, updating or on a flaky connection. Free application tiers usually stop a process that receives no HTTP traffic, which is exactly what a gateway bot looks like. There is no free tier here; the smallest app plan is 1 GB with 0.5 vCPU from $4 a month, which is more than a command bot needs.
Does a Discord bot need an open port or a domain?
No. The connection to Discord is outbound, so a bot needs neither an inbound port nor a certificate. You only need a domain if you add a dashboard or an OAuth callback, and in that case the proxy slot on an app plan handles the hostname and the certificate for you.
Why does my bot show as offline when the process is still running?
Because the gateway connection is what Discord sees, not the process. A blocked event loop - a long synchronous loop, a huge JSON parse, a sync file read - stops heartbeats going out, Discord closes the socket, and the bot appears offline while the container shows perfectly healthy CPU and memory. Move the slow work off the main path.
Do I have to re-register slash commands on every deploy?
No, and you should not. Registrations live on Discord's side until you change them. Run your registration script when the command definitions change, and keep it out of the start command.
How do I move a bot from my PC to a server?
Push the code to GitHub without the token, create the app server, connect the repository, set the environment variables on the Startup tab, set the start command, and start it. Then stop the copy on your PC - two processes on one token fight over the same session and produce very confusing behaviour.
What happens when the bot hits its memory limit?
The container is stopped and restarted clean rather than swapping. You lose anything held only in memory, so persist what matters. If it happens repeatedly, cap the library caches and your own collections before buying more memory; when to upgrade your plan is about telling those two cases apart.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.