Jared Eitnier
Jared Eitnier

Reputation: 7152

jQuery/AJAX set timeout when rate limit of 3rd party API is reached

In my app I make several nested AJAX calls to the LiquidPlanner API that limits requests to 30 requests every 15 seconds. When I hit the limit, I want to set a timeout of sorts to stop sending requests to the API until the 15 seconds have elapsed. This (at the moment) will only be used by one person ever, so multiple clients are not a concern.

Upon hitting the rate limit the response is:

{
    "type":"Error",
    "error":"Throttled",
    "message":"34 requests, exceeds limit of 30 in 15 seconds. Try again in 7 seconds, or contact [email protected]"
}

Here is some code, simplified for brevity:

$.getJSON('/dashboard/tasks/123, function(tasks) {
    $.each(tasks, function(t, task) {
        $.getJSON('/dashboard/project/987, function(project) {
            $.getJSON('/dashboard/checklist-items/382983, function(checklist-items) {
                // form some html here
            });
        });
    });
});

So at any point in this process I could hit the limit and need to wait until the timeout has completed.

I am also open to suggestions to better form the requests instead of nesting them.

Upvotes: 0

Views: 1586

Answers (3)

Mikk3lRo
Mikk3lRo

Reputation: 3496

Another solution that probably prevents hammering better is a queue - however you need to be aware that the order of requests could be significantly different using this method. And that only one request will ever run at a time (so total response times may increase significantly depending on the use case).

//Keep track of queue
var queue = [];

//Keep track of last failed request
var failed_request = false;

function do_request(url, callback) {
    //Just add to queue
    queue.push({
        url:url,
        callback:callback
    });
    //If the queue was empty send it off
    if (queue.length === 1) attempt_fetch();
}

function attempt_fetch() {
    //If nothing to do just return
    if (queue.length === 0 && failed_request === false) return;

    //Get the url and callback from the failed request if any,
    var parms;
    if (failed_request !== false) {
        parms = failed_request;
    } else {
        //otherwise first queue element
        parms = queue.shift();
    }
    //Do request
    $.getJSON(parms.url, function(response) {
        //Detect throttling
        if (response.type === 'error' && response.error === 'throttled') {
            //Store the request
            failed_request = parms;
            //Call self in 15 seconds
            setTimeout(function(){
                attempt_fetch();
            }, 15000);
        } else {
            //Request went fine, let the next call pick from the queue
            failed_request = false;
            //Do your stuff
            parms.callback(response);
            //And send the next request
            attempt_fetch();
        }
    }
}

...your logic still remains largely unchanged:

do_request('/dashboard/tasks/123', function(tasks) {
    $.each(tasks, function(t, task) {
        do_request('/dashboard/project/987', function(project) {
            do_request('/dashboard/checklist-items/382983', function(checklist_items) {
                // form some html here
            });
        });
    });
});

Disclaimer: Still completely untested.

Upvotes: 2

Mikk3lRo
Mikk3lRo

Reputation: 3496

Write a wrapper that will detect the rate-limited response:

//Keep track of state
var is_throttled = false;

function my_wrapper(url, callback) {
    //No need to try right now if already throttled
    if (is_throttled) {
        //Just call self in 15 seconds time
        setTimeout(function(){
            return my_wrapper(url, callback);
        }, 15000);
    }
    //Get your stuff
    $.getJSON(url, function(response) {
        //Detect throttling
        if (response.type === 'error' && response.error === 'throttled') {
            /**
             * Let "others" know that we are throttled - the each-loop
             * (probably) makes this necessary, as it may send off
             * multiple requests at once... If there's more than a couple
             * you will probably need to find a way to also delay those,
             * otherwise you'll be hammering the server before realizing
             * that you are being limited
             */
            is_throttled = true
            //Call self in 15 seconds
            setTimeout(function(){
                //Throttling is (hopefully) over now
                is_throttled = false;
                return my_wrapper(url, callback);
            }, 15000);
        } else {
            //If not throttled, just call the callback with the data we have
            callback(response);
        }
    }
}

Then you should be able to rewrite your logic to:

my_wrapper('/dashboard/tasks/123', function(tasks) {
    $.each(tasks, function(t, task) {
        my_wrapper('/dashboard/project/987', function(project) {
            my_wrapper('/dashboard/checklist-items/382983', function(checklist_items) {
                // form some html here
            });
        });
    });
});

Disclaimer: Totally untested - my main concern is the scope of the url and callback... But it's probably easier for you to test.

Upvotes: 1

GPicazo
GPicazo

Reputation: 6676

As far as design patterns for chaining multiple requests, take a look at the chaining section in the following article: http://davidwalsh.name/write-javascript-promises . Basically, you could create a service that exposes a method for each type of request, which returns the promise object and then chain them together as needed.

As far as you question about setting a timeout, given the information you provided, it is a bit difficult to advice you on it, but if that is absolutely all we have, I would create a request queue ( a simple array that allows you to push new requests at the end and pop the from the head ). I would then execute the known requests in order and inspect the response. If the response was a timeout error, set a timeout flag that the request executor would honor, and if successful, either queue additional requests or create the html output. This is probably a pretty bad design, but is all I can offer given the information you provided.

Upvotes: 1

Related Questions