Why This Tutorial?
Many public webhook proxy services, such as those by @lewisakura, are being overwhelmed by front-page Roblox games and the requests sent by others. By deploying your own proxy, you can ensure a more stable and reliable experience for your webhooks.
Important Note:
- Account Creation: This tutorial assumes you already have a Cloudflare account. I’m not walking you through that—it’s self-explanatory.
- Request Limit: You are limited to 100,000 requests per day using the free tier of Cloudflare.
Step 1: Sign up for Cloudflare
To get started, sign up for Cloudflare if you haven’t already: https://dash.cloudflare.com/
Step 2: Navigate to Cloudflare Workers
Under your Cloudflare dashboard, go to Compute (Workers) > Workers & Pages.
View Image
Step 3: Access the Compute Page
Under the Compute page, you should now see this:
Press the Create button under Workers & Pages as shown in the image above.
Step 4: Create a Worker
You should now see this page. Press the Create Worker button.
Step 5: Name and Deploy the Worker
Name your worker and then press the Deploy button.
Step 6: Edit the Code
On the newly created worker page, press the Edit Code button.
Step 7: Insert and Deploy Your Code
aste the following code into your Cloudflare Worker.js file. Once done, copy the worker’s URL and replace https://discord.com/api/webhooks/
with your new worker URL.
Important Note:
The version with the Queue respects Discord’s rate limits and will queue your requests once the limit is reached. If you wish to handle your own rate limiting, use the version without the queue.
View Code w/ no Queue
THIS VERSION DOES NOT RESPECT RATE LIMIT YOU ARE EXPECTED TO HANDLE THAT ON YOUR OWN IF USING THIS VERSION.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
try {
const url = new URL(request.url)
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json'
}
// Handle webhook requests
if (url.pathname.startsWith('/webhooks/')) {
const webhookPath = url.pathname.replace('/webhooks/', '')
const discordWebhookUrl = `https://discord.com/api/webhooks/${webhookPath}`
const response = await fetch(new Request(discordWebhookUrl, {
method: request.method,
headers: request.headers,
body: request.body
}))
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: corsHeaders
})
}
// Handle non-webhook requests
return new Response(JSON.stringify({
error: 'Not Found',
message: 'Invalid webhook path'
}), {
status: 404,
headers: corsHeaders
})
} catch (error) {
return new Response(JSON.stringify({
error: 'Internal Server Error',
message: error.message
}), {
status: 500,
})
}
}
View Code w/ Queue
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
// rate limit tracking per webhook for ya bud
const webhookLimits = new Map();
class WebhookRateLimiter {
constructor() {
this.queue = [];
this.processing = false;
this.requests = [];
this.shortRequests = []; // For 5/2s limit
this.shortWindowSize = 2000; // 2 second window
this.shortMaxRequests = 5; // 5 requests per 2s
this.longWindowSize = 60000; // 1 minute window
this.longMaxRequests = 30; // 30 requests per minute
}
cleanup() {
const now = Date.now();
this.shortRequests = this.shortRequests.filter(time => now - time < this.shortWindowSize);
this.requests = this.requests.filter(time => now - time < this.longWindowSize);
}
canProcess() {
this.cleanup();
// Check both rate limits
return this.shortRequests.length < this.shortMaxRequests &&
this.requests.length < this.longMaxRequests;
}
getWaitTime() {
const now = Date.now();
let waitTime = 0;
// Check short window (2s) limit (discord limits you to 5 requests per 2 seconds per webhook)
if (this.shortRequests.length >= this.shortMaxRequests) {
waitTime = Math.max(waitTime, this.shortWindowSize - (now - this.shortRequests[0]));
}
// Check long window (1min) limit
if (this.requests.length >= this.longMaxRequests) {
waitTime = Math.max(waitTime, this.longWindowSize - (now - this.requests[0]));
}
return waitTime;
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
if (!this.canProcess()) {
// Wait for the longer of the two wait times (discord limits you to 5 requests per 2 seconds per webhook)
const waitTime = this.getWaitTime();
await new Promise(r => setTimeout(r, waitTime));
continue;
}
const { request, resolve, reject } = this.queue.shift();
try {
const response = await request();
// Track request for both limits (2 per 2s or 30 per min)
const now = Date.now();
this.shortRequests.push(now);
this.requests.push(now);
// hit rate limit so wait and retry (either 2s or 1min)
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '5') * 1000;
this.queue.unshift({ request, resolve, reject });
await new Promise(r => setTimeout(r, retryAfter));
continue;
}
resolve(response);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
async queueRequest(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ request: requestFn, resolve, reject });
this.processQueue();
});
}
}
async function handleRequest(request) {
try {
const url = new URL(request.url)
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json'
}
// webhook requests
if (url.pathname.startsWith('/webhooks/')) {
const webhookPath = url.pathname.replace('/webhooks/', '')
const discordWebhookUrl = `https://discord.com/api/webhooks/${webhookPath}`
// get / create rate limiter for webhook
if (!webhookLimits.has(webhookPath)) {
webhookLimits.set(webhookPath, new WebhookRateLimiter());
}
const limiter = webhookLimits.get(webhookPath);
const response = await limiter.queueRequest(async () => {
return fetch(new Request(discordWebhookUrl, {
method: request.method,
headers: request.headers,
body: request.body
}))
});
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: corsHeaders
})
}
// for non-webhook requests return 404 (I don't know why you are sending a non-webhook request to this proxy lol)
return new Response(JSON.stringify({
error: 'Not Found',
message: 'Invalid webhook path'
}), {
status: 404,
headers: corsHeaders
})
} catch (error) {
return new Response(JSON.stringify({
error: 'Internal Server Error',
message: error.message
}), {
status: 500,
})
}
}
Example for URL:
To use your Cloudflare Worker as a proxy, modify your webhook URL as follows:
Original Webhook URL:
https://discord.com/api/webhooks/1/ABC123
Updated Webhook URL (Using Your Worker URL):
https://this-is-a-tutorial.cloudflare.workers.dev/webhooks/1/ABC123
Could not figure it out? That’s okay!
There are other options that offer one-click deployments or simply require swapping out the URL, which may be better suited for some use cases. I recommend this tutorial by @StarVSK