Background Information | What are rate limits?
The most common posts relating to rate limits you see here on DevForum are specifically about DataStores.
This can be frustrating and honestly annoying. For example when a player leaves my game I want to make sure their data is saved. But what if I get rate limited because I’ve exceeded the number of requests within the past minute to the DataStore API? Well there’s really no easy way to handle this.
I made this module to solve a similar problem relating to using MarketplaceService’s :GetProductInfo()
. As it turns out this function is rate limited to 100 requests per minute. I was requesting 120 times within the same minute. That led to the creation of this module.
Basic Usage
Here’s example usage showing how I solved my own problem using this module:
local HTTP = game:GetService("HttpService")
local RequestQueue = require(script.RequestQueue)
local catalogData = HTTP:GetAsync("https://catalog.rprxy.xyz/v1/search/items?category=Accessories&subcategory=HairAccessories&creatortargetid=1&limit=60")
catalogData = HTTP:JSONDecode(catalogData)
-- Create a new queue
--- param1: Should debugging be enabled?
--- param2: What if a request gets rejected? How should it be handled?
----- This examples uses RETRY which will requeue a request after a certain period of time
--- param3: How many seconds before requeuing the rejected request?
local queue = RequestQueue.new(false, RequestQueue.RejectionHandling.RETRY, 60)
-- Called once the queue has resolved all requests
queue:resolved(function(data)
print(data) -- Successfully prints all the data from :GetProductInfo()!
end)
-- catalogData.data contains 60 entries
for _, v in pairs(catalogData.data) do
-- Enqueue a request and pass in a handler function as well as some parameters
-- The returned value is added to the finalized data table in the :resolved() method
queue:enqueue(function(id, infoType)
return game:GetService('MarketplaceService'):GetProductInfo(id, infoType)
end, v.id, Enum.InfoType.Asset)
end
This example on its own isn’t actually rate limited. However, if I was to press Run → Stop → Run then you would see rate limits were enforced and the data isn’t printed until 60 seconds after the 2nd Run since the retry time is set to 60 seconds.
API
RequestQueue.new(debugEnabled: boolean?, rejectionHandling: number?, retryRate: number?, maxRetries: number?) -> Queue
Create a new Queue object with the provided settings.
Queue:enqueue(handler: function(...: any?) -> any?, ...: any?) -> Promise
Enqueue a request that will return a Promise.
Queue:deferredEnqueue(seconds: number, handler: function(...: any?) -> any?, ...: any?) -> void
Defer a request then enqueue after the specified amount of time.
Queue:resolved(callback: function(resolvedRequests: { any }) -> void) -> void
Hook a callback function to be executed after all requests in the queue are either resolved or rejected (depending on set RejectionHandling).
RejectionHandling
RejectionHandling.FAIL
: Cause an error if a request fails
RejectionHandling.WARN
: Create a warning in the output if a request fails
RejectionHandling.RETRY
: Retry a request after the specified amount of time
Promises
For more information on the promises used within this module see here.
Download
Source
--[[
RequestQueue by AstrealDev
Easily handle rate limits
------------------------------------------------------------------------
Example:
local HTTP = game:GetService("HttpService")
local RequestQueue = require(script.RequestQueue)
local catalogData = HTTP:GetAsync("https://catalog.rprxy.xyz/v1/search/items?category=Accessories&subcategory=HairAccessories&creatortargetid=1&limit=60")
catalogData = HTTP:JSONDecode(catalogData)
-- :GetProductInfo() has a rate limit of 100 requests per minute so retrying every 60 seconds
-- will fix this issue
local queue = RequestQueue.new(true, RequestQueue.RejectionHandling.RETRY, 60)
-- Called upon a queue being fully resolved
queue:resolved(function(data)
print(data)
end)
for _, v in pairs(catalogData.data) do
queue:enqueue(function(id, infoType)
return game:GetService('MarketplaceService'):GetProductInfo(id, infoType)
end, v.id, Enum.InfoType.Asset)
end
]]
local Promise = require(script.Promise)
local RequestQueue = {
RejectionHandling = {
FAIL = 0, -- FAIL: Will error if an enqueue fails
WARN = 1, -- WARN: Will warn if an enqueue fails
RETRY = 2 -- RETRY: Will attempt to retry an enqueue by deferring it based on provided retryRate
},
}
local QueueClass = {}
--[[
Get a new Queue object
]]
function RequestQueue.new(debugEnabled: boolean?, rejectionHandling: number?, retryRate: number?, maxRetries: number?)
local self = setmetatable({}, {__index = QueueClass})
self._rejectionHandling = rejectionHandling
and (rejectionHandling >= 3 or rejectionHandling < 0)
and 0
or rejectionHandling
or RequestQueue.RejectionHandling.FAIL
self._queued = 0
self._resolved = {}
self._debug = debugEnabled or false
self._retries = 0
self._maxRetries = maxRetries or math.huge
self._retryRate = (self._rejectionHandling == RequestQueue.RejectionHandling.RETRY) and (retryRate or 15) or 15
return self
end
--[[
Enqueue a request
]]
function QueueClass:enqueue(handler, ...)
local params = {...}
self._queued += 1
local result
local promise = Promise.new(function(resolve, reject)
local success, err = pcall(function()
result = handler(unpack(params))
end)
if (success) then
resolve(result)
else
reject(err)
end
end)
promise:andThen(function(res)
if (self._debug) then print('Enqueue was successful') end
self._queued -= 1
table.insert(self._resolved, res)
if (self._queued == 0) then
if (self._onResolution) then
local returnData = self._resolved
if (#returnData == 1) then
returnData = returnData[1]
elseif (returnData == 0) then
returnData = nil
end
self._onResolution(returnData)
end
end
end):catch(function(err)
if (self._rejectionHandling == RequestQueue.RejectionHandling.FAIL) then
error(err)
elseif (self._rejectionHandling == RequestQueue.RejectionHandling.WARN or self._debug) then
warn('Enqueue failed | ' .. err)
end
self._queued -= 1
if (self._rejectionHandling == RequestQueue.RejectionHandling.RETRY) then
if (self._retries > self._maxRetries) then
warn(string.format('Enqueue has failed over %d times and will potentially never resolve.', self._maxRetries))
end
self:deferredEnqueue(self._retryRate, handler, unpack(params))
self._retries += 1
end
end)
return promise
end
--[[
Defer a request then enqueue it
]]
function QueueClass:deferredEnqueue(seconds: number, handler, ...)
local params = {...}
Promise.delay(seconds):andThen(function()
self:enqueue(handler, unpack(params))
end)
end
--[[
Hook a callback to be called whenever all requests in the queue are fully resolved
]]
function QueueClass:resolved(callback: (resolvedRequests: { any }) -> void)
self._onResolution = callback or (function() end)
end
return RequestQueue