To host a Discord bot 24/7, run it on a machine that never sleeps and put it under a supervisor that restarts it after a crash and starts it again after a reboot. The machine can be a bot host or platform at $1 to $7 a month, a VPS at $4 to $6 a month, or a free tier if you can live with its catch. The supervisor is systemd, pm2 or Docker, and this guide has a tested setup for each.
Free still exists in 2026, but every free option comes with a condition. Oracle Cloud may reclaim Always Free VMs it considers idle, free bot hosts make you click "renew" every day or every few days, and Replit, Heroku and Glitch no longer keep a bot online for free. If people rely on the bot, pay the few dollars and spend the effort on monitoring instead.
I checked every price on this page in September 2026. I tested the code on September 27, 2026 with discord.js 14.27.0 on Node.js 22.23 and 24.21, pm2 7.0.4, systemd 257 on Debian 13, Docker 29.4 and sqlite3 3.46.
Why a Discord bot goes offline
A normal bot keeps a WebSocket connection open to Discord's gateway, and everything it hears about (messages, members joining, slash commands) arrives over that connection. When the process stops, the connection closes and the bot shows as offline. That is why a bot started from your laptop disappears when the laptop sleeps, and why platforms that put idle apps to sleep don't suit bots.
There is one exception. If your bot only answers slash commands, buttons and modals, Discord can send those to an HTTPS endpoint instead of the gateway. Then nothing has to stay running between requests, and a serverless function can do the job. The section on HTTP interactions covers it.
Hosting options compared (September 2026)
Free options
| Option | What you get | Stays online? | The catch |
|---|---|---|---|
| Oracle Cloud Always Free | 2 AMD VMs with 1 GB RAM each, plus Arm capacity worth 2 OCPUs and 12 GB RAM | Yes | Card needed to sign up; idle instances may be reclaimed; "out of host capacity" errors |
| Google Cloud free tier | 1 e2-micro VM in Oregon, Iowa or South Carolina, 30 GB disk | Yes | Billing account needed; 1 GB outbound a month; the public IPv4 address may be billed |
| bot-hosting.net Free | 1 bot, 256 MB RAM, 25% CPU, 512 MB storage | Only if you renew every 4 days | Manual renewal |
| fps.ms Free | 128 MB RAM, 25% CPU, 250 MB storage | Only if you renew every 24 hours | Personal use only |
| Cloudflare Workers Free | 100,000 requests a day, 10 ms CPU per request | Nothing to keep online | Slash commands, buttons and modals only |
| Your own PC or a Raspberry Pi | Whatever the machine has | While it's on | Your power, internet and OS updates |
Paid options
| Option | Cheapest plan that runs a bot all month | Worth knowing |
|---|---|---|
| Sparked Host | $1.00/mo: 512 MB RAM, 50% CPU, 5 GB | Unmanaged; FFmpeg doesn't work on its bot plans, so no music bots |
| Cybrancee | $1.99/mo: 512 MB RAM, 15 GB | $2.99 for 1 GB; 90-day money-back guarantee |
| bot-hosting.net Starter | $1.99/mo: 1 GB RAM, 2 bots | Backups kept 3 days; Starter+ at $2.99 has 2 GB and 4 bots |
| PebbleHost | $3.00/mo: 1 GB RAM | Node.js, Python and Java; automatic 7-day backups |
| Railway Hobby | $5/mo, includes $5 of usage | Billed per second: $10 per GB of RAM and $20 per vCPU per month |
| Fly.io | about $1.94 per 30 days for shared-cpu-1x with 256 MB in Ashburn | Card required; other regions cost more |
| Render | $7/mo background worker, 512 MB | The free tier has no background workers |
| Heroku | $5/mo Eco or $7/mo Basic, 0.5 GB RAM | An app with only an Eco worker dyno doesn't sleep |
| Replit Reserved VM | $15/mo: 0.5 vCPU, 2 GB RAM | Replit's always-on deployment type |
| DigitalOcean | $4/mo: 512 MB RAM, 10 GB disk; $6/mo for 1 GB | You run the server |
| Hetzner Cloud CX23 | €5.49/mo plus €0.50 for IPv4: 2 vCPU, 4 GB RAM | Germany and Finland only; US locations start at $20.49 |
Prices are as listed in September 2026, and VAT or sales tax may be added depending on where you live. fps.ms also sells paid plans from €0.29 a month, with a €10 minimum top-up.
Free options and their catches
Oracle Cloud Always Free
Every Oracle Cloud account gets two AMD micro VMs (1/8 of an OCPU and 1 GB RAM each) and 1,500 OCPU hours plus 9,000 GB hours of Arm (Ampere A1) capacity a month. Oracle's page says that equals 2 OCPUs and 12 GB of RAM for Always Free accounts, so older guides that promise 4 OCPUs and 24 GB are out of date. Either shape is plenty for a bot.
The catches come from Oracle's own pages:
- Sign-up needs a credit card or a debit card that works like one. Prepaid and virtual cards are refused, and Oracle may place a temporary authorization hold on the card.
- Oracle "may reclaim" idle Always Free instances. Idle means that over 7 days the 95th-percentile CPU use is under 20%, network use is under 20%, and on Arm shapes memory use is under 20%. A small bot fits that description, so plan for it.
- Creating an Arm instance often fails with "out of host capacity". Oracle's advice is to try another availability domain or wait.
- If you build Arm instances larger than the free allowance during the 30-day trial, all your Arm instances are disabled when the trial ends and deleted 30 days later unless you upgrade.
Oracle suits a hobby bot if you want to learn Linux and keep your backups somewhere else.
Google Cloud's e2-micro
Google's free tier includes one e2-micro VM a month in us-west1, us-central1 or us-east1, 30 GB of standard disk and 1 GB of outbound data from North America. A billing account is required. Watch the IP line on your first invoice: Google's network pricing page lists external IPv4 addresses on standard VMs at $0.005 an hour, about $3.60 a month, and describes the free allowance for them as one hour a month. A bot needs outbound internet access, so it needs that address or a paid NAT gateway.
Free bot hosts
Both free hosts in the table keep a bot online only while you keep renewing it by hand: every 4 days on bot-hosting.net, every 24 hours on fps.ms. Miss an fps.ms renewal and the bot goes offline, with its files kept for 28 days. fps.ms also says free hosting is for personal use and that it shuts down commercial bots.
That works for a bot you are learning with, not for one a community depends on. Other free hosts exist; whichever you try, read its renewal and commercial-use rules before you give it your bot token.
What happened to Replit, Heroku and Glitch
Many older "host your bot free forever" tutorials use one of these three, often with a pinging service to keep the app awake. None of that works today:
- Heroku stopped offering free dynos from November 28, 2022. The cheapest plan is Eco at $5 a month for 1,000 dyno hours shared across your Eco dynos, and an app that runs only an Eco worker dyno doesn't sleep, so one worker can run a bot for the whole month. In February 2026 Heroku said it is moving to a "sustaining engineering model" focused on stability and security, with no change for customers who pay by card.
- Replit removed "Always On" on January 1, 2024. The free Starter plan's one published app goes down after 30 days, and the always-on deployment type, a Reserved VM, starts at $15 a month.
- Glitch shut down project hosting on July 8, 2025.
No server at all: HTTP interactions
If your bot only responds to slash commands, context menu commands, buttons and modals, you can set an Interactions Endpoint URL in the Developer Portal. Discord then sends every interaction to that URL as an HTTPS request. Discord has an official tutorial for running this on Cloudflare Workers, whose free plan allows 100,000 requests a day and 10 ms of CPU time per request. Nothing waits between requests, so there is nothing to keep online.
The limits, from Discord's docs: an app receives interactions either over HTTP or over the gateway, never both. Without a gateway connection there are no gateway events either, so no member joins, messages or reactions. And the first response must arrive within 3 seconds, after which the interaction token stays valid for 15 minutes of follow-ups.
Scheduled jobs can skip the bot process too. The Twitch go-live notifier on Rivals League is a scheduled function that runs every minute and posts to Discord through webhooks, so there is nothing to keep alive between runs. A stream alert bot that polls can run the same way.
Paid options that stay online
Bot hosts at $1 to $3 a month
Sparked Host, Cybrancee, PebbleHost and bot-hosting.net sell plans made for bots, priced in the table above. You upload the code through a web panel (Sparked uses a Pterodactyl panel with SFTP, PebbleHost gives FTP access), set the start command and run it from there. Check two things before you buy: the RAM, and whether FFmpeg works if the bot plays audio. Sparked's bot plans say it doesn't.
Pick one of these for a small bot when you don't want to run a Linux server. The host holds your token, so use one you trust and reset the token if you move away.
Railway, Fly.io and Render
All three can run a bot as a long-running process, like Heroku's worker dynos above, but the details differ.
- Railway runs a service until you stop it. At its rates, a bot holding 200 MB of RAM around the clock uses about $2 of memory a month, plus CPU, against the $5 of usage included in Hobby. The Free plan's $1 of monthly credit covers about 100 MB for a month before CPU is counted. Railway's optional Serverless mode only sleeps a service after 5 or more minutes without outbound traffic, and a connected bot keeps sending heartbeats to Discord.
- Fly.io only auto-stops Machines that have services configured, so a bot app with no
[http_service]or[[services]]section keeps running. The shared-cpu-1x preset with 256 MB costs about $1.94 per 30 days in Ashburn and about $3.19 with 512 MB, and some regions cost up to about 60% more. Every organization needs a card on file, and the free trial ends after 2 machine hours or 7 days. - Render runs bots as background workers from $7 a month for 512 MB. Its free instance type only covers web services, static sites, Postgres and Key Value. A free web service spins down after 15 minutes without inbound traffic, may be restarted at any time, and loses local files, including a SQLite database, whenever that happens.
A VPS
A VPS is a small Linux server you control. DigitalOcean's $4 droplet (512 MB) or $6 droplet (1 GB) is enough for a small bot. Hetzner's CX23 costs more since a June 15, 2026 price change (€5.49, up from €3.99) but gives you 2 vCPUs and 4 GB, only in Germany and Finland. Hetzner's US locations don't offer the CX or Arm plans and start at $20.49 a month, so for a US bot DigitalOcean is the cheaper of the two.
A VPS runs as many bots as fit in its RAM and lets you choose every version. In return, security updates, backups and monitoring are your job.
Which hosting is best for a Discord bot?
- Learning, or a bot for a few friends: a free bot host, or Oracle Cloud if you want to learn Linux. Accept the renewals or the reclaim risk.
- A small bot your community uses every day: a $1 to $3 bot host or Railway Hobby, plus an uptime monitor.
- Several bots, a database, or full control: a $4 to $6 VPS with systemd or Docker.
- Only slash commands and buttons: HTTP interactions on Cloudflare Workers.
- A bot that takes payments or runs a league: not a free tier. A few dollars a month is small next to an outage during a match day or a billing run.
How to host a Discord bot 24/7 on a VPS
The rest of this guide sets up a discord.js bot on a Linux server so it keeps running 24/7. Three things matter: code that shuts down cleanly and logs its errors, a supervisor that restarts it and starts it at boot, and a way to find out when it's down. A Python bot needs the same setup; the systemd section shows the one line that changes. If you don't have a bot yet, start with the discord.js guide.
Step 1: Make the bot safe to restart
This is a complete index.js for a bot that only logs in. Add your commands and handlers to it; the parts around them are what keep it running.
const { Client, Events, GatewayIntentBits, Status } = require('discord.js');
const token = process.env.DISCORD_TOKEN;
if (!token) {
console.error('DISCORD_TOKEN is not set. Add it to .env or to your host\'s environment settings.');
process.exit(1);
}
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, (readyClient) => {
console.log(`Logged in as ${readyClient.user.tag}, in ${readyClient.guilds.cache.size} servers`);
});
// Log problems instead of letting one failed reply take the whole bot down.
client.on(Events.Error, (error) => console.error('Client error:', error));
process.on('unhandledRejection', (error) => console.error('Unhandled promise rejection:', error));
// Optional: tell an uptime monitor "still connected" every 5 minutes.
// The ping is skipped while the gateway connection is down, so the monitor alerts you.
const heartbeatUrl = process.env.HEARTBEAT_URL;
if (heartbeatUrl) {
setInterval(async () => {
const connected = client.ws.shards.size > 0 && client.ws.shards.every((shard) => shard.status === Status.Ready);
if (!connected) return;
try {
await fetch(heartbeatUrl, { signal: AbortSignal.timeout(10_000) });
} catch (error) {
console.error('Heartbeat failed:', error.message);
}
}, 5 * 60 * 1000);
}
// Shut down cleanly: pm2 sends SIGINT, systemd and Docker send SIGTERM.
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, async () => {
console.log(`${signal} received, closing the Discord connection`);
await client.destroy();
process.exit(0);
});
}
client.login(token).catch((error) => {
console.error('Login failed:', error.message);
process.exit(1);
});What each part is for:
- The token check exits with an error code and a clear message, so the supervisor's log tells you what's wrong instead of showing a stack trace.
- The error handlers stop one failed interaction reply from crashing the process. Since Node.js 15, an unhandled promise rejection ends the process by default, and every restart costs a new login to Discord (more on why that matters below).
- The heartbeat checks each gateway shard's status rather than
client.isReady(). In discord.js 14.27,isReady()stays true while a dropped connection is reconnecting, so it can't tell you the bot is cut off. - The signal handlers close the gateway connection before exiting, so a restart or deploy shows up as a clean stop in the logs.
Step 2: Put the token in .env
DISCORD_TOKEN=paste-your-bot-token-here
HEARTBEAT_URL=https://hc-ping.com/your-check-idThree rules for this file:
- No quotes around values. Node's
--env-file, systemd and Docker Compose strip them, butdocker run --env-filepasses them through literally. In my test it turned"abc123"into a token that starts and ends with a quote mark, which Discord will reject as invalid. - Keep it out of Git. Put
.envandnode_modulesin.gitignore. GitHub reports Discord bot tokens it finds in public repositories to Discord through its secret scanning partner program, and a leaked token has to be reset either way; the bot token guide covers the reset. - Lock it down on the server with
chmod 600 .env, so only the bot's user can read it.
On a PaaS or bot host, put the same variables in the platform's environment settings instead of a file. Node's --env-file flag, used by the pm2 setup below, is no longer experimental from Node 24.10 and 22.21. When a variable is set both in the environment and in the file, the environment wins.
Step 3, option A: systemd
systemd already runs every other service on Ubuntu and Debian, so it's the option with the fewest moving parts. Create a user for the bot, put the code in /opt/mybot and install the dependencies:
Then create the service file:
[Unit]
Description=My Discord bot
Wants=network-online.target
After=network-online.target
# Never give up; the backoff below keeps a crash loop slow.
StartLimitIntervalSec=0
[Service]
Type=simple
User=mybot
WorkingDirectory=/opt/mybot
EnvironmentFile=/opt/mybot/.env
ExecStart=/usr/bin/node index.js
Restart=always
# First retry after 5 s, then roughly doubling up to 5 minutes (systemd 254+).
RestartSec=5s
RestartSteps=6
RestartMaxDelaySec=5min
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.targetStart it, enable it at boot and watch the log:
Notes from testing it:
ExecStartneeds the full path to Node. Runcommand -v nodeand use what it prints./usr/bin/nodeis where distribution and NodeSource packages put it. If you installed Node with nvm, the path is inside a home directory, and a system-wide Node install is the better choice for a service.- The restart backoff gave retries after 5, 10, 20, 40 and 77 seconds in a crash loop, heading for the 5-minute cap.
RestartStepsandRestartMaxDelaySecneed systemd 254 or newer: Ubuntu 24.04 has 255 and Debian 13 has 257. Debian 12 (252) and Ubuntu 22.04 (249) ignore both lines with an "Unknown key" warning, so on those setRestartSec=2mininstead. ProtectSystem=fullmakes/usr,/bootand/etcread-only for the bot, while/opt/mybotstays writable for a SQLite file.systemctl stopsent SIGTERM, the bot logged "SIGTERM received" and the unit stopped in under 50 ms.
For a discord.py bot in a virtual environment, change one line:
The -u stops Python from buffering output, so print() lines reach journalctl straight away. I tested this start line with discord.py 2.7.1 on Python 3.13.
Step 3, option B: pm2
pm2 is a process manager for Node.js with its own log viewer, and a common choice in bot tutorials. Install it, then put this file next to index.js:
module.exports = {
apps: [
{
name: 'mybot',
script: 'index.js',
cwd: __dirname,
node_args: '--env-file=.env',
// Only ever one copy: two processes with the same token both answer every event.
instances: 1,
exec_mode: 'fork',
// Wait longer after each crash (up to 15 s), and give up after 10 crashes
// in a row that happen within a minute of starting.
exp_backoff_restart_delay: 1000,
min_uptime: '60s',
max_restarts: 10,
// Give the bot time to close its Discord connection on stop/restart.
kill_timeout: 5000,
time: true,
},
],
};pm2 startup prints one sudo command. Run it, and pm2 brings the bot back after a reboot from the list pm2 save stored. I tested that by restarting pm2's systemd service, and the bot came back online.
What the settings did in testing:
exp_backoff_restart_delaygrows the wait between restarts up to pm2's maximum of 15 seconds and resets once the bot has stayed up for 30 seconds.min_uptimeandmax_restartsare what stop a crash loop. With them, a bot that crashed right after starting was restarted 9 times in about 70 seconds and then left alone.pm2 listshowed it as "waiting restart" rather than "errored", which is worth knowing when you look. Without the two lines, pm2 kept restarting it every 15 seconds indefinitely, which is about 5,760 logins a day.- Stopping sends SIGINT, which
index.jshandles.kill_timeoutgives it 5 seconds before SIGKILL instead of pm2's default 1.6 seconds. - Never use cluster mode or more than one instance for a gateway bot. Each copy opens its own connection and answers the same events.
View logs with pm2 logs mybot. pm2 doesn't rotate its log files by itself; pm2 install pm2-logrotate does, rotating at 10 MB and keeping 30 files by default. After you upgrade Node.js, run pm2 unstartup and then pm2 startup again, because the boot script points at the old Node binary.
Step 3, option C: Docker
Docker makes sense if you already use it on the server. You need three files next to index.js, package.json and package-lock.json:
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "index.js"]node_modules
.env
.gitservices:
bot:
build: .
restart: unless-stopped
env_file: .env
logging:
driver: localWhy it's set up this way:
.dockerignorekeeps.envout of the image. The token only reaches the container at runtime throughenv_file, so you can push the image without leaking it. I checked that.envwasn't inside the built image.USER noderuns the bot as the unprivileged user that the official Node image already includes. The code in/appbelongs to root, so the bot can't write there; a bot that keeps a SQLite file needs a volume thenodeuser can write to.logging: driver: localbecause Docker's defaultjson-filedriver doesn't rotate logs, and Docker's docs warn it can fill the disk. Thelocaldriver rotates by default.- Node runs as process 1 in the container, and signals only stop it if the code handles them.
index.jsdoes, sodocker compose stopfinished at once instead of waiting out Docker's grace period and then killing the bot.
Crash loops and Discord's 1,000 logins a day
Discord allows each bot 1,000 IDENTIFY calls, meaning new gateway sessions, per 24 hours. Resuming a dropped session doesn't count. If you hit the limit, Discord ends all of the bot's sessions, resets its token and emails the owner. Every restart of a crashed process opens a new session, so a supervisor that restarts too eagerly can get your token reset.
| Supervisor | What it did in a crash loop (tested) | Restarts per day at most |
|---|---|---|
| systemd, unit above | Waits of 5, 10, 20, 40 and 77 seconds, capped at 5 minutes | About 290 |
| pm2, file above | Waits growing to 15 seconds, then stopped after 10 quick crashes | About 10, then it waits for you |
Docker, unless-stopped |
100 ms doubling to 1 minute, but the delay resets after 10 seconds of uptime | About 2,900 if the bot crashes 30 seconds after each start |
The Docker row is the one to watch. A bot that logs in, runs for 11 seconds and crashes was restarted about 7 times every 90 seconds in my test, because each run counted as a successful start. on-failure:5 stops that, but the count only resets when the container is recreated, so the sixth crash of that container's life, even months later, leaves it down. In my test, a container on on-failure:2 that ran 11 seconds before each crash stayed down after its third crash. With Docker, the error handlers in index.js and the uptime alert below do most of the protecting.
Logs
- systemd:
journalctl -u mybot -ffollows the log, andjournalctl -u mybot --since "1 hour ago"shows the recent past. The journal caps its own disk use by default, at 10% of the file system up to 4 GB. - pm2:
pm2 logs mybot, plus pm2-logrotate as described above. - Docker:
docker compose logs -f bot, rotated by thelocaldriver.
The lines in index.js are enough to tell a crash from a deploy: a clean stop logs "SIGTERM received" or "SIGINT received" first, and a crash doesn't.
Know when the bot is down
Restarts handle crashes. They don't help when the whole server is down, when Discord has reset the token, or when the process is running but can't reconnect. For those you need a check from outside:
- Healthchecks.io has a free Hobbyist plan with 20 checks. Create a check with a 5-minute period and some grace time, put its ping URL in
HEARTBEAT_URL, and you get an alert when the pings stop. - Uptime Kuma is open source and self-hosted, with Push monitors that work the same way and notifications to Discord and many other services. Run it somewhere other than the bot's server, or it goes down with the bot.
Back up what the bot stores
If the bot keeps data in SQLite, copying the file with cp while the bot writes to it can give you a broken copy, according to SQLite's own documentation. The sqlite3 command-line tool (sudo apt install sqlite3) has a .backup command that makes a consistent copy of a live database:
#!/bin/sh
set -e
mkdir -p /var/backups/mybot
# wait up to 10 s for a write in progress instead of failing with "database is locked"
sqlite3 -cmd ".timeout 10000" /opt/mybot/data/bot.db ".backup '/var/backups/mybot/bot-$(date +%F).db'"
# keep two weeks of daily copies
find /var/backups/mybot -name 'bot-*.db' -mtime +14 -deleteCreate it with sudo, make it executable with sudo chmod +x /usr/local/bin/mybot-backup.sh, then run it every night at 04:15 by adding 15 4 * * * /usr/local/bin/mybot-backup.sh with sudo crontab -e. The script lives outside /opt/mybot because root runs it, and the bot's user can change anything in /opt/mybot. Keeping the date format inside the script avoids cron's rule that % must be escaped in a crontab line.
I tested it while another process was writing to the database. Without the .timeout line, 2 of 5 backups failed with "database is locked"; with it, all 10 runs worked and every copy passed SQLite's integrity check. Then copy the backups off the server, because a backup on the same VPS disappears with it.
Updating without surprises
Updating the code:
Running git and npm as the mybot user keeps file ownership right. Never run the old and the new version at the same time with the same token, not even for a quick test, because both will answer every command.
Updating Node.js. Node.js 20 reached end of life on April 30, 2026. Node.js 22 is supported until April 30, 2027 and Node.js 24 until April 30, 2028. After an upgrade, check that the ExecStart path still points at the right binary, or rerun pm2 unstartup and pm2 startup.
Updating the library, and keeping up with Discord. Bots often stop working without anyone touching them, because Discord changed something. Three recent examples from Discord's change log:
- Since February 23, 2026, pinning messages needs the new Pin Messages permission; Manage Messages alone no longer does it.
- Since March 1, 2026, voice connections must support Discord's end-to-end encryption (DAVE). For discord.js that means @discordjs/voice 0.19 or newer, which requires Node.js 22.12 or newer. For discord.py it means 2.7.0 or newer.
- From November 16, 2026, the endpoint that lists a server's channels will leave out channels the bot can't view.
Read the release notes before you update, and try the update first with a separate test application in a private server, so it never shares the live bot's token. If the bot has already stopped responding, Discord bot not working? walks through the usual causes.
If you'd rather not run it yourself
Everything above is an evening's work to set up and a little time every month to keep up. If you'd rather hand that off, my Care plan is $49 a month for managed hosting, uptime monitoring, library and security updates when Discord changes things, and daily database backups. It also works for bots I didn't build. If your bot is already broken or stuck on an old host, a Bot Rescue gets it running again for a fixed $149, and you pay nothing if I can't fix it. You can ask about Care here.