On October 4, 2026, an attacker took control of the Discord bot token belonging to Double Counter, an anti-raid and verification bot built by Tellter SAS and installed in more than 600,000 Discord communities. Using that single stolen credential, the attacker spammed invite links across roughly 50 large servers and pulled approximately 12 GB of database records in about 25 minutes, exposing data tied to an estimated 28 million Discord IDs and usernames, 27 million IP addresses and coarse location records, 25 million user-agent hashes, and 1 million email addresses. Discord itself was not breached. The company moved quickly to block new installs of the compromised app while it investigated, and it was explicit that the failure sat with a third-party developer, not its own infrastructure.
That distinction matters if you run, or plan to run, a Discord bot for a gaming community, a Minecraft server, an esports league, or any project with real users attached. A bot token is a bearer credential: whoever holds it can act as your bot everywhere it’s installed, read messages in channels it can see, and in Double Counter’s case, reach into the database behind it. This tutorial walks through locking down a Discord bot the way the Double Counter incident shows you should have from day one: isolated credentials, scoped OAuth2 permissions, a real secrets manager instead of a stray .env file, rate limiting, token rotation, and a response plan for the day a credential leaks anyway. By the end you’ll have a complete, working reference project you can adapt for discord.js or discord.py.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Discord Bot Token Security Matters Right Now
Discord’s own safety guidance is blunt about what a leaked bot token means: whoever has it can log in as your bot and act in every server where it’s installed, no second factor required. In a public statement on the incident, Discord Support said: “We’re aware of a security incident involving Double Counter, a third-party bot developer. We’ve confirmed Discord’s systems were not breached. We are preventing new server installs of Double Counter’s app while we investigate the incident. We’re working closely with Double Counter and will take further actions as appropriate to protect our users.” That is the correct read of the incident: the platform held, the application didn’t.
According to Gadget Review’s reporting on disclosures from Tellter SAS, the attacker’s initial access point was an old, still-active server running a vulnerable version of Metabase, Double Counter’s business-intelligence tool. From there, the attacker reached the bot’s production token and the database behind it. Cybersecurity News reported the attacker used that token to post invite links across the roughly 50 large communities where Double Counter was installed, before the provider could revoke it. The lesson for anyone running a Discord bot isn’t “don’t get breached” (every provider eventually faces an incident); it’s that the blast radius of a single leaked token can scale to tens of millions of accounts if the bot is popular enough. Dexerto noted Discord responded by disabling new installations of the app entirely while the review was underway, a step the platform has reserved for serious incidents.
Discord’s platform-wide safety guidance says it plainly: “If your bot’s code is publicly available, please remove your bot’s token from the text to prevent it from being compromised.” The same page adds: “If your account’s token has been compromised, reset your password to generate a new token,” and “You should never give your account password or token to anyone.” Every step in this guide exists to make those three sentences unnecessary to act on in a panic, because the controls are already in place before a leak happens.
| Double Counter breach data point | Reported figure | Why it matters for your bot |
|---|---|---|
| Discord IDs and usernames exposed | ~28 million | Shows how fast exposure scales once a bot is installed in hundreds of thousands of servers |
| IP addresses and coarse location records | ~27 million | Confirms the bot was logging more telemetry than its core anti-raid function needed |
| User-agent hashes exposed | ~25 million | Device fingerprinting data stored alongside identity data increases re-identification risk |
| Email addresses exposed | ~1 million | A smaller but high-value subset tied directly to phishing risk |
| Database volume copied | ~12 GB in ~25 minutes | Illustrates how quickly a compromised credential with database access can be drained |
| Communities running the bot | 600,000+ | One token compromise, near-unlimited blast radius |
Prerequisites: What You Need Before You Start
This tutorial is written for a Node.js bot built on discord.js, with discord.py equivalents noted where the approach differs. Install and verify the following before you touch the Developer Portal. Check exact versions with node -v, npm list discord.js, and python -m pip show discord.py.
| Tool | Version used in this guide | Purpose |
|---|---|---|
| Node.js | 24.21.0 LTS (“Krypton”) | Runtime for the discord.js bot and secrets-manager SDKs |
| discord.js | 14.27.0 | Primary Discord API wrapper used in the code examples |
| dotenv | 18.0.7 | Local-only environment variable loading, never for production secrets |
| discord.py | 2.7.1 | Python alternative wrapper, referenced for equivalent patterns |
| Git | 2.40 or newer | Version control with a .gitignore that excludes secrets |
| A secrets manager account | Any of AWS Secrets Manager, HashiCorp Vault, or Doppler | Production token storage, covered in Step 3 |
| A Discord Developer Portal account | N/A | Required to register the application and generate the token |
You’ll also need a server (VPS, container host, or managed platform) you control for deployment, and ideally a second, low-privilege Discord server you own for testing commands before they touch production communities. Nothing here requires paid tooling beyond what most small teams already run; the free tiers of Doppler and HashiCorp Vault’s open-source edition are enough for a single bot.
Step 1: Create a Dedicated, Isolated Bot Application
Go to the Discord Developer Portal and create a new application dedicated to this bot alone. Don’t reuse an application that also issues OAuth2 logins for a website or shares credentials with another project. Isolation limits what a leaked token can touch: if this application only ever does one job, a compromised token only ever does that one job in the hands of an attacker.
Under the Bot tab, note two settings before moving on. First, disable “Public Bot” unless you genuinely intend for other server owners to add it; a private bot cuts down unsolicited installs and the associated attack surface. Second, review “Requires OAuth2 Code Grant” only if your bot does something that needs user-level authorization beyond basic installation; most moderation and utility bots don’t need it, and leaving it off reduces the number of token types you have to protect.
Discord requires verification once a bot is installed in 100 or more servers, which forces a baseline review of what data the bot collects and how. Don’t treat verification as a one-time hoop to clear: use the same checklist internally every time you add a new data-collecting feature, well before you hit that threshold.
Step 2: Generate Your Token and Keep It Out of Source Control
Generate the bot token from the Bot tab and copy it once. Discord will not show it to you again without a reset. The single most common way tokens leak is a developer committing it directly into a repository, a config file, or a support-forum post while debugging. Set up your .gitignore before you write a single line of bot code, not after.
# .gitignore
.env
.env.*
!.env.example
*.log
config/secrets.json
node_modules/
# .env.example (safe to commit — placeholder only)
DISCORD_TOKEN=
DISCORD_CLIENT_ID=
DISCORD_GUILD_ID_TEST=
LOG_WEBHOOK_URL=
Load the real .env locally with dotenv 18.0.7 for development only. Never ship a .env file to a production server through your deployment pipeline; it’s one misconfigured public bucket or leaked container image away from becoming the next Double Counter headline. Production tokens belong in a secrets manager, which is Step 3.
Step 3: Store the Token in a Secrets Manager, Not a Bare .env File
A .env file is fine on your laptop. On a production server it’s a plaintext credential sitting next to your application code, readable by anything with filesystem access, and almost never rotated because rotating it means redeploying manually. Pull the token from a secrets manager at process start instead, and cache it in memory only.
// secrets.js — fetch the bot token from AWS Secrets Manager at boot
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
export async function loadBotToken() {
const command = new GetSecretValueCommand({ SecretId: "prod/discord-bot/token" });
const response = await client.send(command);
const secret = JSON.parse(response.SecretString);
if (!secret.token) {
throw new Error("Secrets Manager response missing 'token' field");
}
return secret.token;
}
The equivalent pattern works with HashiCorp Vault’s KV v2 engine or Doppler’s CLI injection (doppler run -- node index.js), which avoids writing the secret to disk or environment history at all. Whichever you choose, the goal is the same: the token lives in exactly one authoritative place, access to read it is logged, and rotating it doesn’t require touching application code.
Step 4: Scope OAuth2 Permissions to the Bare Minimum
When you generate the invite URL for your bot, Discord lets you pick both OAuth2 scopes and a permissions integer. Most bots only need the bot and applications.commands scopes. Avoid requesting identify, email, or connections scopes unless a specific feature genuinely needs user-level account data beyond what the bot scope already provides, since every scope you request is data you’re now responsible for protecting if your infrastructure is ever compromised the way Double Counter’s was.
| Permission | Typical use case | Risk if over-granted |
|---|---|---|
| Administrator | None for most bots | Highest risk; grants every permission including role and channel deletion |
| Manage Roles | Self-assignable role bots, verification bots | Can escalate privileges for arbitrary members if logic has a bug |
| Manage Messages | Moderation, anti-spam bots | Can delete evidence or legitimate messages if compromised |
| Kick/Ban Members | Moderation bots only | Can be used to mass-remove members if the token is stolen |
| Read Message History | Logging, search, moderation bots | Exposes private conversation content if the bot’s data store is breached |
| Send Messages, Embed Links | Nearly every bot | Low risk in isolation; mainly a spam vector if the token leaks |
Build the invite link with only the bits your current feature set needs, and revisit the permission integer every time you ship a new command. A verification bot like Double Counter, whose entire job is checking for alts and raids, arguably never needed to retain IP addresses and user-agent hashes for 28 million accounts; collecting more than the feature requires turns an application compromise into a mass data breach.
Step 5: Disable Unused Privileged Gateway Intents
Gateway intents control which events Discord streams to your bot. Privileged intents, specifically GUILD_MEMBERS, GUILD_PRESENCES, and MESSAGE_CONTENT, require explicit opt-in in the Developer Portal because they expose more member data than a basic bot needs. Enable only what your current commands actually consume.
// index.js — request only the intents the bot actually uses
import { Client, GatewayIntentBits, Partials } from "discord.js";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
// Only add MessageContent if a command genuinely parses message text.
// Only add GuildMembers if you need the member cache for moderation logic.
],
partials: [Partials.Message, Partials.Channel],
});
client.once("ready", () => {
console.log(`Logged in as ${client.user.tag}, intents: ${client.options.intents.bitfield}`);
});
If a feature is retired, remove its intent in the same pull request. A bot carrying privileged intents it no longer uses is exactly the kind of unnecessary data exposure that turns a routine token leak into a large-scale breach.
Step 6: Build Application-Level Rate Limiting
Discord enforces its own per-route rate limits and returns limit metadata with every response; discord.js’s REST manager already queues and backs off automatically against those. That protects you from Discord banning your bot for hammering its API, but it does nothing to stop a compromised token, or a buggy command, from spamming every server the bot is in. You need a second layer of rate limiting at the application level, independent of Discord’s own throttling.
// rateLimiter.js — simple in-memory token bucket per guild
const buckets = new Map();
export function allowAction(guildId, { capacity = 10, refillPerSecond = 1 } = {}) {
const now = Date.now() / 1000;
let bucket = buckets.get(guildId);
if (!bucket) {
bucket = { tokens: capacity, lastRefill: now };
buckets.set(guildId, bucket);
}
const elapsed = now - bucket.lastRefill;
bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * refillPerSecond);
bucket.lastRefill = now;
if (bucket.tokens < 1) return false;
bucket.tokens -= 1;
return true;
}
Call allowAction(guildId) before any action that posts messages, sends invites, or writes to your database, and short-circuit if it returns false. This single function would have capped the damage in a Double-Counter-style incident: instead of an attacker's stolen token blasting invites to 50 servers in minutes, the bucket throttles it to a trickle, buying time for your monitoring to catch the anomaly (Step 11) before the blast radius grows.
Step 7: Add Per-Guild and Per-User Command Cooldowns
Rate limiting the bot's outbound actions handles a compromised token; cooldowns on inbound commands handle abusive or automated users trying to exploit a command for data gathering or denial of service. Keep this logic separate from Step 6 since the two problems have different sources and different fixes.
// cooldowns.js — per-user, per-command cooldown
const lastUsed = new Map();
export function onCooldown(userId, commandName, seconds = 5) {
const key = `${userId}:${commandName}`;
const now = Date.now();
const last = lastUsed.get(key) || 0;
if (now - last < seconds * 1000) return true;
lastUsed.set(key, now);
return false;
}
// usage inside an interaction handler
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (onCooldown(interaction.user.id, interaction.commandName)) {
return interaction.reply({ content: "Slow down a bit.", ephemeral: true });
}
// ...route to command handler
});
Pair cooldowns with command-level permission checks (interaction.memberPermissions.has()) so that even an authenticated member can't invoke moderation or data-export commands they shouldn't have access to, regardless of what the raw Discord role permissions technically allow.
Step 8: Set Up a Token Rotation and Revocation Runbook
Discord's own safety page is direct about the fix for a compromised token: "If your account's token has been compromised, reset your password to generate a new token." For a bot application, the equivalent action is resetting the bot token from the Developer Portal's Bot tab, which immediately invalidates the old one. The problem most teams have isn't knowing this step exists; it's not having practiced it before an incident forces their hand.
- Write down, in a shared runbook, exactly who has Developer Portal access and can trigger a reset.
- Store the new token procedure as code: a deploy script that pulls the freshly rotated token from your secrets manager and restarts the bot process, so rotation doesn't require manually editing a server.
- Rotate on a schedule (quarterly is reasonable for most bots) even without a suspected leak, so the rotation path is tested and muscle-memory rather than improvised under pressure.
- Rotate immediately, not after investigation, the moment you suspect exposure: a token in a public repo, a leaked log file, or a compromised developer laptop.
If you use a secrets manager with versioning (AWS Secrets Manager and Vault both support this), keep the previous token version accessible for a short grace window only if your architecture needs a rolling restart; otherwise, revoke it immediately after confirming the new token is live.
Step 9: Segment Bot Infrastructure From Your Other Services
According to reporting on the Double Counter incident, the attacker's path in was an old, still-running server hosting a vulnerable Metabase instance, not a flaw in the bot's core logic. That's a textbook asset-inventory failure: a secondary analytics tool, probably forgotten by whoever originally set it up, sat on the same network as production credentials. Run your bot process on infrastructure isolated from unrelated internal tools, with its own firewall rules, its own database user with the minimum grants it needs, and no shared network path to admin dashboards, BI tools, or anything you're not actively patching.
Maintain a simple, living inventory: every server, container, and managed service that can reach the bot's database or its secrets manager entry, when it was last patched, and who owns it. Decommission anything that isn't actively used. An old Metabase box nobody remembered was running is precisely the kind of asset that an inventory review, done even once a quarter, would have flagged before it became an entry point.
Step 10: Minimize, Encrypt, and Expire Stored User Data
The Double Counter breach exposed IP addresses, coarse location data, and user-agent hashes at a scale (27 million and 25 million records respectively) that suggests the bot retained this telemetry indefinitely rather than for the minimum window its anti-raid function required. Apply three rules to anything your bot logs about members:
- Collect only what a shipping feature needs. If you're not actively using IP data for an anti-raid heuristic today, don't store it "in case it's useful later."
- Encrypt data at rest using your database or disk provider's native encryption, and encrypt any field containing IPs, emails, or device fingerprints at the application layer as a second layer.
- Set explicit retention windows. A rolling 30 or 90-day expiry on raw telemetry (verification logs, join timestamps, IP history) limits how much an attacker can steal even if they do get database access, because most of the data they'd want has already been purged.
A verification bot in particular doesn't need a permanent record of every IP a member has ever connected from; it needs enough recent history to detect an alt-account pattern. Design the schema around that narrower requirement from the start.
Step 11: Add Logging and Real-Time Anomaly Alerts
The Double Counter attacker reportedly moved from initial access to exfiltrating 12 GB of data in about 25 minutes. That's a window where real-time alerting, not next-morning log review, is the only thing that could realistically have limited the damage. Wire a simple webhook alert into any action your rate limiter (Step 6) blocks, and into any spike in outbound message volume.
// alerts.js — post anomalies to a monitoring webhook immediately
export async function alertAnomaly(message, context = {}) {
await fetch(process.env.LOG_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: `⚠️ Bot anomaly: ${message}`,
embeds: [{ description: JSON.stringify(context).slice(0, 1900) }],
}),
});
}
// example trigger inside the rate limiter
if (!allowAction(guildId)) {
await alertAnomaly("Guild action rate limit hit", { guildId, timestamp: Date.now() });
}
Point this at a dedicated, private Discord channel, a PagerDuty integration, or a Slack webhook, whatever your team actually checks in real time. The goal is a detection window measured in minutes, not the hours or days it typically takes to notice an incident through routine log review.
Step 12: Audit Dependencies for Known Vulnerabilities
A bot's attack surface isn't limited to its own code; it includes every npm or PyPI package it depends on, and every auxiliary service (like the Metabase instance in the Double Counter incident) running nearby. Run a dependency audit as part of your deployment pipeline, not as an occasional manual check.
# Node.js dependency audit, fail the build on known high-severity issues
npm audit --audit-level=high
# Python equivalent
pip install pip-audit
pip-audit
Extend the same discipline to any supporting service, analytics dashboard, admin panel, or internal tool connected to the same network as your bot's database. Keep a patch cadence for those services even if they feel peripheral to the "real" bot, since the Double Counter incident shows that peripheral services are exactly where attackers look first.
Step 13: Run a Token-Leak Fire Drill
The best test of Steps 1 through 12 is a drill where you deliberately simulate a leaked token and time how long it takes your team to detect, revoke, rotate, and notify. Schedule this quarterly, alongside your scheduled rotation from Step 8.
- Revoke the current token from the Developer Portal without warning the on-call engineer in advance.
- Time how long until the bot going offline triggers an alert (if it doesn't trigger one at all, that's your first finding).
- Walk through generating a new token, pushing it to the secrets manager, and redeploying, timing the full recovery.
- Confirm your audit log shows who accessed the secrets manager entry and when, both during the drill and historically.
- Write down what broke. A fire drill that goes perfectly on the first try usually means the drill wasn't realistic enough.
Teams that have never rehearsed a token rotation under pressure consistently take hours to recover from a real one; teams that drill quarterly typically cut that to minutes, which is the entire difference between an incident that stays contained and one that reaches 28 million accounts.
The Complete Hardened Bot: A Working Reference Project
Here's a condensed version of the pieces above wired together into a single entry point. Treat it as a starting skeleton, not a drop-in production system; your rate limits, intents, and data retention windows should match your bot's actual feature set.
// index.js — reference project combining token loading, intents, rate limiting, and alerting
import { Client, GatewayIntentBits } from "discord.js";
import { loadBotToken } from "./secrets.js";
import { allowAction } from "./rateLimiter.js";
import { onCooldown } from "./cooldowns.js";
import { alertAnomaly } from "./alerts.js";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (onCooldown(interaction.user.id, interaction.commandName)) {
return interaction.reply({ content: "Slow down a bit.", ephemeral: true });
}
if (!allowAction(interaction.guildId)) {
await alertAnomaly("Guild action rate limit hit", { guildId: interaction.guildId });
return interaction.reply({ content: "Try again shortly.", ephemeral: true });
}
// route to your actual command handlers here
});
client.once("ready", () => {
console.log(`Ready as ${client.user.tag} in ${client.guilds.cache.size} guilds`);
});
(async () => {
const token = await loadBotToken();
await client.login(token);
})();
Expected output when this starts cleanly against a test server looks like:
$ node index.js
Ready as RaidGuard#0421 in 1 guilds
If loadBotToken() throws instead, you'll see the secrets manager error surface before the bot ever attempts a login, which is the correct failure mode: a misconfigured secret should stop startup, not fall back silently to an empty or cached token.
Discord.py Equivalent: Token Storage and Rate Limiting in Python
If your stack is Python rather than Node.js, the same controls apply with discord.py 2.7.1, just with different syntax. Loading the token from a secrets manager instead of hardcoding it follows the same boot-time pattern used in Step 3.
# secrets.py — fetch the bot token from HashiCorp Vault at boot
import hvac
import discord
from discord.ext import commands
def load_bot_token():
client = hvac.Client(url="https://vault.internal:8200")
secret = client.secrets.kv.v2.read_secret_version(path="discord-bot/token")
token = secret["data"]["data"].get("token")
if not token:
raise RuntimeError("Vault response missing 'token' field")
return token
intents = discord.Intents.default()
intents.message_content = False # only enable if a command genuinely parses text
intents.members = False # only enable if moderation logic needs the member cache
bot = commands.Bot(command_prefix="!", intents=intents)
if __name__ == "__main__":
bot.run(load_bot_token())
For per-guild rate limiting, discord.py's commands.cooldown decorator covers the inbound-command case from Step 7 directly, but you still need your own bucket for outbound actions like the one in Step 6, since the built-in decorator only throttles how often a user can invoke a command, not how often the bot itself posts messages or writes to a database on a compromised token's behalf.
# rate_limiter.py — token bucket per guild, mirrors the discord.js version
import time
_buckets = {}
def allow_action(guild_id, capacity=10, refill_per_second=1.0):
now = time.monotonic()
bucket = _buckets.setdefault(guild_id, {"tokens": capacity, "last_refill": now})
elapsed = now - bucket["last_refill"]
bucket["tokens"] = min(capacity, bucket["tokens"] + elapsed * refill_per_second)
bucket["last_refill"] = now
if bucket["tokens"] < 1:
return False
bucket["tokens"] -= 1
return True
Both ecosystems converge on the same architecture: token fetched from a secrets manager at process start, privileged intents trimmed to what's actually used, and a bucket-based limiter sitting between any inbound trigger and any outbound action the bot can take. The language doesn't change the threat model; a leaked discord.py token is exactly as dangerous as a leaked discord.js one.
Understanding OAuth2 Code Grant vs. a Plain Bot Token
It's worth being precise about a distinction that trips up a lot of first-time bot developers. A plain bot token, the one you generated in Step 2, authenticates your application as itself and is enough for the overwhelming majority of moderation, utility, and game-community bots. OAuth2 Code Grant is a separate flow where your bot asks an individual Discord user to authorize it, generating a user-scoped access token and refresh token distinct from the bot's own credential.
You only need Code Grant if your bot does something like linking a user's Discord account to an external service, reading their connected accounts, or acting on their behalf outside of server-level bot permissions. Every bot that adds this flow takes on a second category of credential to protect: refresh tokens, which if leaked let an attacker impersonate individual users rather than just the bot application. If your feature set doesn't need it, don't enable it. The fewer credential types your infrastructure handles, the smaller the surface a single compromised service, like the old Metabase box in the Double Counter incident, can expose.
Common Pitfalls That Lead to Token Leaks
- Pasting the token into a support channel or forum post while debugging. This remains one of the most common leak vectors; treat any token you've ever typed outside your own secrets manager as compromised and rotate it.
- Committing a working
.envfile "just this once" during a crunch deploy. Git history retains it forever, even after a later commit deletes the file; a leaked token needs rotation, not just removal from the latest commit. - Running the same token across staging and production. A compromised staging environment, which usually has weaker controls, becomes a direct path to production if they share credentials.
- Over-scoping OAuth2 permissions "to be safe." Administrator permission on a moderation bot isn't safer, it's a single point of failure that turns any bug or leak into full server control.
- Letting old, unused infrastructure keep network access to the bot's database. The Double Counter incident traces back to exactly this: a forgotten Metabase instance nobody had decommissioned.
- Logging the token itself during debugging. A
console.log(client.token)left in during a late-night debugging session can end up in a log aggregator with far looser access controls than your secrets manager. - Skipping dependency audits because "the bot is small." Bot size has no bearing on whether a dependency has a known remote-code-execution vulnerability.
Troubleshooting Guide
Even with the hardening above in place, you'll hit operational snags. Here are the most common ones and how to resolve them.
- Bot logs in but immediately disconnects with a 4004 "Authentication Failed" code. The token was reset or revoked after your process cached it; pull a fresh copy from the secrets manager and restart rather than retrying the same in-memory value.
- "Missing Access" errors on commands that used to work. A server admin likely changed role permissions, or you tightened the invite's permission integer in Step 4; re-check the bot's current role against what the command actually requires.
- Secrets manager calls intermittently time out on boot. Add retry-with-backoff around
loadBotToken()rather than failing immediately; a cold-start secrets manager call under load can legitimately take a few seconds. - Rate limiter blocks legitimate bursts of activity. Your
capacityandrefillPerSecondvalues in Step 6 are per-guild defaults; tune them per guild size rather than using one global setting for a 50-member server and a 50,000-member one. - Privileged intent toggle in the Developer Portal is greyed out. Bots in 100+ servers must pass Discord's verification review before privileged intents can be enabled; budget time for that review before you plan a feature around it.
- Webhook alerts from Step 11 go silent after a few days. Discord webhooks can be deleted or rate-limited if they fire too frequently; route high-volume alerts through a dedicated monitoring tool instead of a raw webhook once volume grows.
- npm audit reports vulnerabilities with no available fix. Check whether the vulnerable code path is actually reachable in your bot's usage; if it is, pin to a patched fork or remove the dependency rather than ignoring the finding.
- Token rotation breaks a long-running interaction (like a paginated embed) mid-session. Design stateful interactions to re-authenticate gracefully, or accept that a rotation briefly interrupts in-flight sessions and schedule rotations for low-traffic windows.
Advanced Tips for Production Bot Fleets
If you operate more than one bot, or a bot large enough to need sharding, a few patterns beyond the basics above are worth adopting early rather than retrofitting later.
Give each shard or process its own scoped database credentials rather than one shared superuser connection string; a compromised shard then can't reach data outside its own responsibility. Use short-lived, auto-expiring secrets manager leases (Vault's dynamic secrets feature does this natively) instead of static long-lived tokens wherever your secrets manager supports it, so a leaked credential has a built-in expiry even if nobody notices the leak immediately. Separate your command-handling process from any process that has direct database write access, communicating over an internal queue instead, so a vulnerability in command parsing can't directly reach stored user data. Finally, treat your secrets manager's own access logs as a first-class alert source, not an afterthought: an unexpected read of the bot token secret outside your normal deploy window is itself a signal worth paging someone over, independent of anything the bot's own telemetry shows.
Frequently Asked Questions
Was Discord's platform breached in the Double Counter incident?
No. Discord confirmed its own systems were not breached; the incident involved a third-party bot developer's infrastructure, specifically an outdated Metabase server used for analytics.
How do I know if my Discord bot token has already leaked?
Check your secrets manager or hosting provider's access logs for reads you can't account for, search your own public repositories and any forks for the token string, and treat any token that's ever appeared in a log file, screenshot, or support ticket as compromised regardless of whether you've found direct evidence of misuse.
Does resetting my bot token break existing server installations?
No. The bot remains installed in every server; only the credential your process uses to log in changes. Update the secret in your secrets manager and redeploy, and the bot reconnects under the same identity with the same server installs intact.
What's the difference between OAuth2 scopes and bot permissions?
Scopes (like bot or applications.commands) control what categories of access your application requests during installation. Permissions (like Manage Messages or Kick Members) control what actions the bot can take once installed. You should minimize both independently.
Do I need a paid secrets manager for a small hobby bot?
No. HashiCorp Vault's open-source edition and Doppler's free tier both cover a single bot's needs. The goal is keeping the token out of your deployment environment's plaintext files, not spending money.
How often should I rotate a Discord bot token?
Quarterly rotation on a schedule is a reasonable baseline for most bots, paired with immediate rotation the moment you suspect any exposure, rather than waiting for confirmation of misuse.
Can rate limiting at the application level actually stop a stolen token from being misused?
It can't prevent misuse outright, but it caps the speed and scale of it. A token bucket that limits a guild to a handful of actions per minute turns a potential mass-invite spam event into a contained, slow trickle that your anomaly alerts have time to catch before the damage spreads to dozens of servers.
Should my bot collect IP addresses for anti-raid or anti-alt detection?
Only if that specific feature genuinely requires it, and only with a defined retention window. The scale of IP and user-agent data exposed in the Double Counter breach suggests data was retained well beyond what active anti-raid detection needs; shorter retention limits what's exposed if your infrastructure is ever compromised.





