Your Discord bot was fine on your laptop. You moved it to a cheap host and now it hits 429s it never earned, or Discord stops answering it for a while. Often the problem is not your code. It is the IP address you share with everyone else on that host.
This guide covers when a Discord bot proxy actually helps, how to set one up in discord.py and discord.js, what each library can and cannot send through it, and how to read the errors when it goes wrong.
First, what this guide is for
A bot here means a bot account: an application you created in the Discord developer portal, logging in with its own token. A moderation bot, a utility bot, a bot that posts your deploy notifications. That is the only kind of Discord automation this page helps with.
Automating a normal user account, a “self-bot”, is not allowed. Discord says so plainly: automating user accounts outside the bot API is forbidden and can get the account terminated. Running many accounts, or dodging a ban or a rate limit by changing IPs, is against Discord’s rules too. Spam, mass DMs and harassment are out under Discord’s rules and under our acceptable use policy. A proxy changes where your traffic leaves from. It does not change what Discord allows.
Do you need a proxy for a Discord bot?
Usually not. Most bots run fine straight from the server they live on. A proxy earns its place for network reasons:
- Your host’s IP has a bad reputation you did not create. Free and shared bot hosts put many bots behind one outbound address. Some of Discord’s limits apply per IP, so a neighbour’s mistakes land on you.
- You need a fixed egress IP. Your bot calls your own API or database, and that service only accepts known addresses. Or a company network only lets traffic out through a proxy.
- Your host cannot reach Discord well, or you run the bot from home and would rather not tie it to your home IP.
If none of those is you, stop here and fix the bot instead. A proxy will not make a bot that ignores rate limits behave.
How Discord rate limits a bot, and where the IP comes in
Discord’s rate limit documentation describes three layers. Per-route limits and the global limit of 50 requests a second are counted per bot, by token. Changing the IP does nothing to them, and it should not.
The third layer is counted per IP address. Discord calls it the invalid request limit: an IP that makes 10,000 requests in 10 minutes that end in 401, 403 or 429 is temporarily blocked from the API at the Cloudflare edge. Every bot on that IP shares the count. On a shared host, one broken bot looping on an expired token can get the address blocked for all of you, and your well-behaved bot gets an HTML error page instead of JSON.
That is the honest case for a proxy: an IP of your own, so the only invalid requests counted against it are yours, and other people’s broken bots go back to being other people’s problem. It is not a way around the per-bot limits.
Where invalid requests come from
Both libraries already wait out 429s, so a healthy bot rarely piles them up. The usual sources of a bad count are quieter: a bot that keeps retrying after its token was reset (401), a command that keeps posting into a channel it lost permission for (403), or a job that keeps calling a deleted webhook (404, which Discord also restricts). Log the status of every failed request for a day before you buy anything. If the failures are yours, a new IP only moves the problem to an address you pay for.
discord.py proxy setup
discord.py takes a proxy on the Client (and on commands.Bot, which passes it through) as two arguments: proxy, a URL string, and proxy_auth, an aiohttp.BasicAuth. In the library source both go to every REST call and to the gateway websocket, so the whole bot leaves from the proxy IP.
pip install -U discord.pyimport os
import aiohttp
import discord
client = discord.Client(
intents=discord.Intents.default(),
proxy="http://IP:PORT",
proxy_auth=aiohttp.BasicAuth("USER", "PASS"),
)
@client.event
async def on_ready():
print(f"Logged in as {client.user}")
client.run(os.environ["DISCORD_TOKEN"])Keeping the password in BasicAuth rather than in the URL means you never have to URL-encode an @ or a : in it. The proxy URL scheme is http:// even though Discord is https://; the scheme describes the hop to the proxy. The aiohttp setup page has more on how aiohttp handles proxies underneath.
discord.py through SOCKS5
aiohttp has no SOCKS support of its own. The aiohttp-socks package supplies a connector, and discord.py accepts one through its connector argument. Create it inside a running event loop:
import asyncio
import os
import discord
from aiohttp_socks import ProxyConnector
async def main():
connector = ProxyConnector.from_url("socks5://USER:PASS@IP:PORT")
client = discord.Client(intents=discord.Intents.default(), connector=connector)
async with client:
await client.start(os.environ["DISCORD_TOKEN"])
asyncio.run(main())Use the SOCKS host, port and credentials your dashboard lists for the order. HTTP is simpler and works just as well for Discord; the SOCKS5 vs HTTP guide explains when the difference matters.
discord.js proxy setup: REST yes, gateway no
discord.js sends REST calls through @discordjs/rest, which uses undici and accepts an undici Dispatcher as its agent option. The client passes its rest options straight through, so an undici ProxyAgent is all it takes:
npm i discord.js undici@6// bot.mjs, run with: node bot.mjs
import { Client, Events, GatewayIntentBits } from "discord.js";
import { ProxyAgent } from "undici";
const client = new Client({
intents: [GatewayIntentBits.Guilds],
rest: { agent: new ProxyAgent("http://USER:PASS@IP:PORT") },
});
client.once(Events.ClientReady, (c) => console.log("Logged in as", c.user.tag));
client.login(process.env.DISCORD_TOKEN);Pin undici to the major version discord.js already depends on (6 for discord.js 14 at the time of writing; npm ls undici shows yours). The @discordjs/rest source notes that mixing undici copies breaks file uploads. Credentials in the URL work; undici turns them into the Proxy-Authorization header.
Now the part other guides skip. The gateway websocket, handled by @discordjs/ws, is opened with the ws package and only a handshake timeout: no agent, no proxy option. In current discord.js there is no supported setting that sends the gateway through a proxy. REST goes through the proxy; the gateway leaves from your host’s own IP.
For the shared-host problem that is usually enough, because the invalid-request count is about HTTP responses. If the whole process must leave from one address, for a firewall rule for instance, do it at the network layer on the host rather than inside discord.js, or use discord.py, which proxies both.
Voice is separate in both libraries: audio travels over UDP, and an HTTP proxy only carries TCP.
Static IP or rotating residential for a Discord bot?
Static. A bot holds one gateway websocket open for hours or days, and reconnects and resumes when it drops. A rotating residential gateway gives each new connection a new exit IP, so every reconnect would come from somewhere else, and residential exits are home connections that come and go. Sticky sessions (a dashboard setting) help, but only for a while; the rotating vs sticky guide explains the trade-off.
Two more reasons. An allowlist needs one address, not a pool. And a bot that runs all day moves a steady trickle of events and heartbeats, which a per-gigabyte plan meters forever.
A dedicated datacenter IP is the usual answer. Discord bots live on hosting ranges anyway, so a datacenter address looks exactly like what it is. It costs $3.20 for 30 days, with no per-GB meter to watch. An ISP address is the other static option if you want a consumer-network IP, but a bot rarely needs one. The datacenter pricing page has the longer terms.
Discord bot proxy errors and what they mean
| What you see | Likely cause | What to do |
|---|---|---|
| 407 from the proxy | Wrong username or password, or your IP is not on the allowlist | Recheck the credentials in the dashboard; in aiohttp it shows as ClientHttpProxyError, in undici as “Proxy response (407)” |
| 429 with JSON and X-RateLimit-Scope | A Discord rate limit: user or global means your bot, shared means a busy resource | Wait retry_after; both libraries already do. Shared-scope 429s do not count against you |
| 429 with an HTML page, not JSON | The IP is temporarily blocked at Cloudflare, often shown as error 1015 | Find what is making invalid requests. Behind a proxy, that is your own bot |
| Gateway closes with 4004 | Authentication failed: bad token | Not a proxy problem. Reset the token and stop retrying the old one |
| Gateway closes with 4008 or 4014 | Rate limited on the gateway, or a privileged intent you have not enabled | Send fewer gateway payloads; enable the intent in the developer portal |
| Timeouts, or reconnect loops | The proxy is unreachable, or something between you drops idle connections | Test the proxy with curl first; check the host’s firewall allows the proxy port |
The most important line in that table is the second one. A 429 with an X-RateLimit-Scope of user or global is about your bot, and a new IP will not change it. The 429 guide covers reading Retry-After, and the 407 guide covers the credential side.
Test the proxy on its own before blaming the bot. This endpoint needs no token:
curl -x http://USER:PASS@IP:PORT https://discord.com/api/v10/gatewayA small JSON object with a url in it means the network part is fine. An HTML page instead means the proxy IP itself is blocked, and a 407 means the credentials are wrong.
Running a Telegram bot as well?
The same ideas carry over, with different traps: two separate request objects in python-telegram-bot, webhooks that a proxy cannot help with, and MTProto proxies that are not what you want. The Telegram bot proxy guide walks through them. For picking a line in general, the proxy type guide takes four questions.
Top-ups start at $5.
One shared datacenter IP for 30 days is $2.10. A single gigabyte of residential is $5.50. The balance never expires.