Tutorial · 6 min read

Running a Discord bot through a proxy: discord.py and discord.js

Route a Discord bot through a proxy in discord.py and discord.js, why a shared host can get your bot blocked for others’ errors, and why a static IP wins.

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.py
import 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

Common errors when a Discord bot runs through a proxy
What you seeLikely causeWhat to do
407 from the proxyWrong username or password, or your IP is not on the allowlistRecheck the credentials in the dashboard; in aiohttp it shows as ClientHttpProxyError, in undici as “Proxy response (407)”
429 with JSON and X-RateLimit-ScopeA Discord rate limit: user or global means your bot, shared means a busy resourceWait retry_after; both libraries already do. Shared-scope 429s do not count against you
429 with an HTML page, not JSONThe IP is temporarily blocked at Cloudflare, often shown as error 1015Find what is making invalid requests. Behind a proxy, that is your own bot
Gateway closes with 4004Authentication failed: bad tokenNot a proxy problem. Reset the token and stop retrying the old one
Gateway closes with 4008 or 4014Rate limited on the gateway, or a privileged intent you have not enabledSend fewer gateway payloads; enable the intent in the developer portal
Timeouts, or reconnect loopsThe proxy is unreachable, or something between you drops idle connectionsTest the proxy with curl first; check the host’s firewall allows the proxy port
Close codes are from Discord’s opcodes and status codes documentation. 4004, 4010, 4011, 4012, 4013 and 4014 are not worth reconnecting on.

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/gateway

A 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.

Try it while you read

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.

Published

Filed under

Found a mistake? Tell us in Discord and we will fix the post.

The community layer

Stuck halfway through?

Paste the error in Discord. Someone has hit it before and the answer is usually one message long.

Join the Discord

4,200+monkeys in the Discord

  • Help from humans

    Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.

  • A status bot that tells on us

    Pool health, incidents and maintenance posted automatically. Including the bad days.

  • Deals and free traffic

    Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.

Join the Discord4,200+ monkeys, free to lurk