Discord Webhook Proxy w/ Cloudflare (FREE)


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.


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

image


Step 3: Access the Compute Page

Under the Compute page, you should now see this:

View Image

Press the Create button under Workers & Pages as shown in the image above.


Step 4: Create a Worker

View Image

You should now see this page. Press the Create Worker button.


Step 5: Name and Deploy the Worker

View Image

Name your worker and then press the Deploy button.


Step 6: Edit the Code

View Image

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.

View Image

:warning: 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

:stop_sign: 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

19 Likes

Thanks for that, now my loggers are running at the speed of light!

You’re welcome, Jax! :grin: chars chars chars

2 Likes

Thanks wow! I literally just searched for this since I’ve already had some familiarity with cloudfare workers since I used them for a roblox API proxy. To my surprise you made this the day before I needed it lol. Crazy timing.

Is there anyway to make this respect discords rate limits?

1 Like

Hey @yoolurs,

Yes, you can queue the requests if you’d like (to respect the limits). Based on what I read here, Discord limits you to 5 requests per 2 seconds, but I’m not 1000% sure that’s accurate. During my 30-second test, it seemed to follow this limit, but it may vary.

This simple updated version should respect that limit and queue requests once it’s reached. You may want to handle it differently, but this is a straightforward solution to your problem. I’ve set it up so that each webhook has its own queue. This means that sending to webhooks/123/ABC123 vs webhooks/312/CBA312 should not affect each other’s queues. Based on the Cloudflare worker free-tier limits, this should work fine, as there’s no limit on duration, only CPU time.

Let me know if this works for you, bud! :grin:

View Code
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.windowSize = 2000; // 2 seconds because discord limits you to 5 requests per 2 seconds per webhook 
    this.maxRequests = 5;
  }

  cleanup() {
    const now = Date.now();
    this.requests = this.requests.filter(time => now - time < this.windowSize);
  }

  canProcess() {
    this.cleanup();
    return this.requests.length < this.maxRequests;
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) return;
    this.processing = true;

    while (this.queue.length > 0) {
      if (!this.canProcess()) {
        // waiting for old req to expire
        const waitTime = this.windowSize - (Date.now() - this.requests[0]);
        await new Promise(r => setTimeout(r, waitTime));
        continue;
      }

      const { request, resolve, reject } = this.queue.shift();
      try {
        const response = await request();
        
        // tracking req
        this.requests.push(Date.now());
        
        // hit rate limit so wait and retry
        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
    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,
    })
  }
}
1 Like

Works perfectly thanks! I just tested it out!
Appreciate the assistance a ton. You’re a life saver.

1 Like

Maybe i am wrong, but didnt roblox recently allow direct discord requests?

1 Like

Thats not up to ROBLOX. Discord themselves blocked requests from ROBLOX due to people not following the rate limits. All it really takes is a few bad actors to ruin it for everyone unfortunately.

1 Like

I mean discord recently allowed roblox requests again, i thought.

1 Like

Just came across one issue while doing a for i = 1,100 do loop running requests just to test.
After exactly 37 requests it hits me with an http timed out error (after yielding for a very long time)

I believe discord also has a 30 requests per minute limit on top of the 5 requests every 2 seconds limit. Maybe compensating for both would solve this?

2 Likes

I think this… might work? Its 30 per 1 min and 5 per 2 sec.

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,
    })
  }
}
1 Like

Yup this fixed it. Thanks a ton man.

1 Like

No problem! :grin: wow i need more letters to reply

1 Like

Honestly, I think they might, but it seems to be hit-or-miss. Half the time they work, and half the time they don’t. I’d rather just use a proxy to be 100% sure my requests go through. Plus, the normal webhook api don’t have a queue, so if you hit the rate limit, the request is simply dropped.

1 Like

Great tutorial, but this isn’t necessary anymore as Discord allowed Roblox webhook requests. I’ve removed the proxies and added requests to Discord webhooks to my games, and it’s been working 100% of the time.

Thanks for the feedback; however, I have to disagree with the claim that this is “no longer necessary” If you’re able to only have a small amount of requests, relying on their webhook URL might work fine for you. However, for higher volumes, the lack of a queue means you risk dropped requests when hitting rate limits, making proxies (with a queue) a valuable solution.