A Discord bot is a long-lived process holding one websocket open. The websocket costs almost nothing: a heartbeat every forty-odd seconds and whatever events you subscribed to. A slash-command bot in thirty servers will sit at 80-150 MB of resident memory and use a few per cent of a core, which means the smallest plan on any host is already too big for it.
What costs something is the cache. Every mainstream library keeps a copy of what it has seen - guilds, channels, roles, members, messages, presences - so that your code can read them without a network round trip. That cache scales with your success rather than with your code, which is why a bot that ran fine in ten servers falls over in four hundred without a single line changing. The second thing that costs is audio, because a music bot is really a transcoding service with a chat interface attached.
This post is about telling those apart before you pay for the wrong one.
What a bot actually holds in memory#
Break the number down and it stops being mysterious. On a typical bot the resident set is four things:
| Component | Typical cost | Grows with |
|---|---|---|
| Runtime and libraries | 60-120 MB (Node), 50-90 MB (Python) | Nothing much |
| Guild, channel and role cache | 1-10 MB per few hundred guilds | Number of servers |
| Member and user cache | hundreds of bytes to a couple of KB each | Members, if you cache them |
| Message cache | 200 messages per channel by default in discord.js | Channels multiplied by activity |
| Voice, per active stream | 30-80 MB plus real CPU | Concurrent listeners |
| Your own state | whatever you put in a variable | Usually, quietly, forever |
The member cache is the whole answer on any bot in large servers. A bot in guilds totalling a hundred thousand members, with member caching on and guilds chunked at startup, can add several hundred megabytes before it has done anything useful. The same bot with member caching off sits where it started.
The last row deserves more attention than it gets. Anything you kept in a dictionary because a database felt like overkill is a memory leak with a plan. Cooldown maps keyed by user ID, a Map of recent messages for an anti-spam check, an array of queued songs per guild: all of them are unbounded unless you bounded them. Put a cap and an eviction on every collection your code owns, or accept that the process grows until the container stops it.
Intents decide your memory before your code does#
Gateway intents are the subscription you declare when you connect. They control what Discord sends you, and what it sends you is what the library caches. Getting them right is the single highest-leverage sizing decision available, and it takes one line.
Three intents are privileged, which means they must be switched on in the Developer Portal as well as in your code, and a bot in more than 100 servers must be verified to keep them:
GUILD_MEMBERS- member joins, leaves and updates. This is the one that fills memory. Without it your bot does not receive the member list at all.GUILD_PRESENCES- online status and activity for every member of every guild. Enormously expensive and almost never needed. If you enabled it to show who is online, you almost certainly did not need it.MESSAGE_CONTENT- the text of messages your bot is not directly mentioned in. Needed for prefix commands, not needed for slash commands.
A bot built entirely on slash commands needs GUILDS and very little else. Moving from prefix commands to slash commands is therefore a memory optimisation as well as an interface change, and it removes the verification burden of MESSAGE_CONTENT.
Cutting the cache down#
Both major libraries let you bound the cache explicitly. Do it before you need to, not after an out-of-memory stop.
In discord.js v14, makeCache sets per-manager limits and sweepers evicts on a timer:
const { Client, GatewayIntentBits, Options } = require("discord.js");const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], makeCache: Options.cacheWithLimits({ ...Options.DefaultMakeCacheSettings, MessageManager: 25, PresenceManager: 0, GuildMemberManager: { maxSize: 50, keepOverLimit: (member) => member.id === member.client.user.id, }, }), sweepers: { ...Options.DefaultSweeperSettings, messages: { interval: 3600, lifetime: 1800 }, },});The keepOverLimit line is not decoration. The library needs its own guild member to resolve its permissions, so a hard cap without that exception breaks permission checks in ways that are hard to trace. The documentation also lists several managers that must not be limited at all - guilds, channels and roles among them - because the rest of the library assumes they are complete. Limit messages, members and presences; leave the structural caches alone.
In discord.py the equivalent is on the client constructor:
import discordintents = discord.Intents.default()intents.message_content = Falseintents.members = Falseintents.presences = Falseclient = discord.Client( intents=intents, max_messages=None, # default is 1000, None disables chunk_guilds_at_startup=False, member_cache_flags=discord.MemberCacheFlags.none(),)chunk_guilds_at_startup is the one people miss. With the members intent enabled it defaults to true, and the library requests the full member list of every guild the moment it connects. On a bot in a few hundred large servers that is a long, memory-hungry startup that happens on every restart. Turn it off and fetch the members you actually need with guild.fetch_member(id).
After either change, watch the memory graph in the console for a day rather than trusting the first reading. Startup memory is not steady-state memory, and Node in particular will happily grow towards whatever heap ceiling it thinks it has before the garbage collector gets serious. Why your Node app dies at 2 GB on a 4 GB plan explains that ceiling and how to set it deliberately.
Music and voice bots are a different animal#
A music bot is not a chat bot with a feature. It is an audio pipeline, and the sizing rules are completely different.
Each concurrent stream involves fetching a source, decoding it, resampling to 48 kHz stereo, encoding to Opus, encrypting and sending 50 packets a second. In practice that means at least one ffmpeg process per stream, often a yt-dlp process to resolve the source first, and a native Opus encoder. On the Node side, @discordjs/voice also needs an encryption package and an Opus library installed alongside it; on the Python side you need ffmpeg on the path and PyNaCl for voice.
Budget roughly:
| Concurrent streams | Memory on top of the base bot | CPU |
|---|---|---|
| 1 | 30-80 MB | 5-15% of a core |
| 5 | 150-400 MB | 0.3-0.8 of a core |
| 20 | 0.6-1.5 GB | 1.5-3 cores, plus spikes |
Two practical consequences. The first is that CPU, not memory, is what runs out on a music bot, and CPU on a shared plan is a hard throttle to the share you bought. A throttled event loop is worse than a slow one: it delays the gateway heartbeat, Discord stops receiving acknowledgements, and the library reconnects. A bot that "randomly disconnects when several people play music" is almost always CPU-starved rather than network-troubled.
The second is that resolving a source with yt-dlp is a spiky, unpredictable cost that has nothing to do with playback, and it writes to disk if you let it cache. On a 5 GB plan that fills up faster than you would expect. Set a cache directory you can clear, or disable caching entirely.
A music bot serving one or two guilds is fine on 1-2 GB with a full core. A public music bot with dozens of concurrent streams belongs on something much larger, and honestly belongs on a machine of its own.
Sharding, and where the memory multiplies#
Discord requires a bot to shard once it is in more than 2,500 guilds. The gateway tells you how many shards it wants: request GET /gateway/bot with your token and read the shards field. Below that threshold, sharding is a solution to a problem you do not have.
How sharding affects memory depends entirely on the library, and this is the detail that catches people out:
- discord.js `ShardingManager` spawns a separate Node process per shard. Each process is a complete bot with its own runtime, its own library instance and its own cache. Sixteen shards is sixteen times the base memory, and cross-shard communication has to go through the manager with
broadcastEval. Budget generously. - discord.py's `AutoShardedClient` runs every shard in one process on a single event loop, so the runtime cost is paid once and the caches are shared. Memory grows with data, not with shard count.
Either way, sharding is the point where a bot stops being a cheap process and becomes infrastructure. If you are heading there, move the shared state out of memory first, because the moment there is more than one process nothing in a variable is reliable any more.
Where the data goes#
The default for a small bot is a JSON file, and it works right up until it does not. Two failure modes, both common:
- Writing the whole file on every change. At a few hundred kilobytes and a write per command this is fine; at several megabytes it becomes a visible stall on every interaction, because the event loop is blocked while it serialises.
- Being killed mid-write. A process stopped between
openandcloseleaves a truncated file, and the bot will not start. Always write to a temporary file in the same directory andrenameit into place, which is atomic on any sane filesystem.
Move to SQLite when the file gets awkward, which for most bots is around the point where you want to query rather than load everything. SQLite is a single file, needs no server, and handles a bot's write volume without noticing. Move to a networked database when you have more than one process, when two things write concurrently, or when the dataset stops fitting comfortably in memory.
On the hosting side, app plans include two database slots created in the panel, which generates a host, user and password for you. For a larger or separately scaled store there are standalone PostgreSQL and MongoDB lines. Postgres or MongoDB is the honest comparison, and connection pools and limits covers the mistake most bots make first: opening a connection per command and exhausting the server.
Whatever you choose, the token and the connection string do not belong in the repository. Put them in environment variables on the Startup tab - environment variables and secrets explains why, and what to do the day one leaks.
Plan sizes for real bots#
Using the app hosting ladder as a reference. Every tier gets the same panel, so nothing here is about features.
| Bot | Memory | CPU | Sensible tier |
|---|---|---|---|
| Slash commands, under 50 guilds | 150-250 MB | under 0.2 core | 1 GB, 0.5 vCPU |
| Moderation or logging, a few hundred guilds | 300-700 MB | 0.2-0.5 core | 2 GB, 1 vCPU |
| Economy or levelling with a database | 400 MB - 1 GB | 0.3-0.7 core | 2-4 GB |
| Music, 1-3 concurrent streams | 400-800 MB | 0.5-1 core | 2 GB, 1 vCPU |
| Music, 10+ concurrent streams | 1.5-3 GB | 2-3 cores | 6-8 GB, 2-3 vCPU |
| Sharded, 2,500+ guilds (discord.js) | 250-400 MB per shard | 0.2 core per shard | measure, then multiply |
Disk is rarely the constraint but is worth a glance. A discord.js project with voice support pulls 150-250 MB of node_modules; a Python virtual environment is usually smaller. Install with npm ci --omit=dev so that build-time dependencies do not ship. The real disk risk is a music bot caching audio and a bot writing an unrotated log file, both of which fill a 5 GB plan silently over a couple of months.
Start small. A bot is the easiest workload in hosting to size correctly after the fact, because the memory graph in the console tells you exactly where you sit against the limit and moving tiers changes the limit on the server you already have rather than rebuilding it.
Staying online: restarts, crash loops and deploys#
The thing people actually buy hosting for is that the bot is still running tomorrow. Three details decide whether it is.
An unhandled rejection kills the process. In modern Node an unhandled promise rejection terminates the process by default. Handle it, log it, and make sure the logging happens before the exit:
process.on("unhandledRejection", (error) => { console.error("unhandled rejection:", error);});process.on("SIGTERM", async () => { await client.destroy(); process.exit(0);});Catching SIGTERM and destroying the client matters more than it looks: a bot that is killed without closing its websocket stays visible as online for up to a minute afterwards, which makes a restart look like a much longer outage than it was. Graceful shutdown and health checks has the general pattern.
Hitting the memory limit is not a graceful event. At the container's limit the kernel stops the process and it restarts clean rather than being left to swap. That is a good default - swapping a bot is worse than restarting it - but it means an unbounded cache shows up as a mysterious restart every few hours rather than as a slow decline.
A crash loop gets noticed. A watcher polls every couple of minutes for a server that went offline or whose uptime went backwards; restarts you asked for are not counted. Three unexpected restarts in an hour puts a warning on the server page and opens a ticket automatically, and six leads to suspension. That policy exists because a bot restarting every twenty seconds hammers the gateway and burns identify allowance, and it is worth knowing before you deploy a change at midnight and go to bed. Why your server keeps restarting covers diagnosing the loop itself.
Deploys are the other half. Connecting a GitHub repository gives you two switches: pull on every start, and deploy on push, which restarts only a server that was already running. Both are worth having on a bot, because the alternative is uploading files over SFTP and forgetting which version is live. The walkthrough is in deploy a Node.js app from GitHub, and it applies to Python the same way.
FAQ#
How much RAM does a Discord bot need?
A slash-command bot in a few dozen servers needs 150-250 MB, so the smallest plan available is already generous. Memory only becomes a question when you cache members, cache messages, play audio, or keep state in variables. Measure the steady state for a week before buying more.
Why does my bot use more memory as it joins more servers?
Because the library caches what the gateway sends, and the gateway sends more when there is more. The fix is almost always intents and cache limits rather than a bigger plan: turn off GUILD_PRESENCES, turn off member chunking at startup, and cap the message cache.
Do I need to shard my bot?
Not until Discord requires it, which is above 2,500 guilds. Ask the gateway what it wants with GET /gateway/bot rather than guessing. Sharding early adds processes, complexity and, in discord.js, a multiplied memory bill for no benefit.
Can I run several bots on one plan?
Technically two processes fit in one container, but it is a false economy: they share a memory limit and a CPU throttle, and one crash-looping bot takes the other one down with it. Separate small plans keep the blast radius small and make the graphs readable.
Is a bot better on a VDS?
If you are running six or seven small services it can be cheaper, because memory pools across all of them. For one or two bots it is more work than it saves. A VDS or a game panel works through the crossover properly.
Why does my music bot stutter when several people use it?
CPU. Each stream is a transcode, CPU on a plan is a hard throttle to the share you bought, and once the event loop is starved the audio packets go out late and the gateway heartbeat goes out late with them. Watch the CPU graph while it happens; if it is pinned at your limit, that is your answer.




Comments
Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.