Strategies for dealing with multiple Ajax calls

This post is more than 2 years old.

Let's consider a fairly trivial, but probably typical, Ajax-based application. I've got a series of buttons:

shot1

Each button, when clicked, hits a service on my application server and fetches some data. In my case, just a simple name:

shot2

The code for this is rather simple. (And note - for the purposes of this blog entry I'm keeping things very simple and including my JavaScript in the HTML page. Please keep your HTML and JavaScript in different files!)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>

<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>

<div id="resultDiv"></div>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
	$result = $("#resultDiv");
	
	$(".loadButton").on("click", function(e) {
		var thisId = $(this).data("prodid");
		console.log("going to load product id "+thisId);
		$result.text("");
		$.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
			console.log("back with "+JSON.stringify(res));
			$result.text("Product "+res.name);
		});
	});
});
</script>
</body>
</html>

I assume this makes sense to everyone as it is pretty boiler-plate Ajax with jQuery, but if it doesn't, just chime in below in a comment. Ok, so this works, but we have a small problem. What happens in the user clicks both buttons at nearly the same time? Well, you would probably say the last one wins, right? But are you sure? What if something goes wrong (database gremlin - always blame the database) and the last hit is the first one to return?

Untitled2

What you can see (hopefully - still kinda new at making animated gifs) is that the user clicks the first button, then the second, and sees first the result from the second button and then the first one flashes in.

Now to be fair, you could just blame the user. I'm all for blaming the user. But what are some ways we can prevent this from happening?

One strategy is to disable all the buttons that call this particular Ajax request until the request has completed. Let's look at that version.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>

<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>

<div id="resultDiv"></div>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
	$result = $("#resultDiv");
	
	$(".loadButton").on("click", function(e) {
		//disable the rest
		$(".loadButton").attr("disabled","disabled");
		var thisId = $(this).data("prodid");
		console.log("going to load product id "+thisId);
		$result.text("Loading info...");
		$.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
			console.log("back with "+JSON.stringify(res));
			$(".loadButton").removeAttr("disabled");
			$result.text("Product "+res.name);
		});
	});
});
</script>
</body>
</html>

I've added a simple call to disable all the buttons based on class. I then simple remove that attribute when the Ajax request is done. Furthermore, I also include some text to let the user know that - yes - something is happening - and maybe you should just calm the heck down and wait for it. The result makes it more obvious that something is happening and actively prevents the user from clicking the other buttons.

Untitled3

Another strategy would be to actually kill the existing Ajax request. This is rather simple. The native XHR object has an abort method that will kill it, and jQuery's Ajax methods returns a wrapped XHR object that gives us access to the same method.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>

<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>

<div id="resultDiv"></div>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>

$(document).ready(function() {
	$result = $("#resultDiv");

	var xhr;
	var active=false;

	$(".loadButton").on("click", function(e) {
		var thisId = $(this).data("prodid");
		console.log("going to load product id "+thisId);
		$result.text("Loading info...");
		
		if(active) { console.log("killing active"); xhr.abort(); }
		active=true;
		xhr = $.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
			console.log("back with "+JSON.stringify(res));
			$result.text("Product "+res.name);
			active=false;
		});
	});
});
</script>
</body>
</html>

I use two variables, xhr and active, so that I can track active xhr requests. There are other ways to track the status of the XHR object - for example, via readyState - but a simple flag seemed to work best. Obviously you could do it differently but the main idea ("If active, kill it"), provides an alternative to the first method.

When using this, you can actually see the requests killed in dev tools:

Untitled4

Any comments on this? How are you handling this yourself in your Ajax-based applications?

p.s. As a quick aside, Brian Rinaldi shared with me a cool little UI library that turns buttons themselves into loading indicators: Ladda

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Jonathan Smith posted on 4/3/2015 at 2:31 PM

A user found an issue with multiple AJAX calls in our SPA. We ended up adopting the xhr.abort() approach.

Comment 2 by Larry C. Lyons posted on 4/3/2015 at 3:42 PM

I've used the BlockUI plugin. After including the library, I just use this default script:

$(document).ajaxStart($.blockUI).ajaxStop($.unblockUI);

then do my ajax calls as needed;

Comment 3 by robocat posted on 4/3/2015 at 8:37 PM

A third solution is to only have one ajax call in-flight/active, and queue any further requests and send the requests after you get the first response (also makes webserver coding simpler because there can be no race conditions if state modified for that user).

Ideally requests need to be consolidated together (not very RESTful though).

If deleting the previous message, only delete messages of the same kind.

For example, desktop keyboard up key/down key row selection on a master grid with a child form:
1. Send request of selected row number immediately to give fast response
2. If lots of down presses queued, only need to keep last message.
3. Disable child form while waiting for response.
4. Use a whole page disabling spinner when user saves.

Comment 4 by Rob James posted on 4/4/2015 at 11:03 AM

You can simplify that a little more actually. You don't need the active variable, or check the ready state - all you need to do is

if (xhr) xhr.abort();

Comment 5 by Ryan Guill posted on 4/4/2015 at 12:30 PM

Usually what I do in those situations is send a token with the request that gets echo'd back with the response and if the response's token isn't the latest token i've sent then I ignore the response. I usually use a millisecond unix timestamp for the token but anything unique for that page request works fine.

but I like the idea of cancelling the ajax request. On mobile that could make a difference if you had a lot of this going on.

Comment 6 by Michiel van Eerd posted on 4/4/2015 at 4:05 PM

You could also use promises: when you return a Promise from a then callback, the next then waits on it to resolve. So if you use 1 Promise than you can synchonize tha calls and their result.

See the GitHub Gist:

https://gist.github.com/mic...

The response of getOne always returns after 10 milliseconds, and the response of getTwo always returns after 3000 milliseconds. If you call getTwo first and then immediately getOne, you still get the response of getOne after the response of getTwo.

Comment 7 by Piotr z blog.piotrnalepa.pl posted on 4/4/2015 at 5:05 PM

Another option is to debounce click event callback. With this implemented you start sending AJAX request after some (short, about 200-300ms) time. That way you could create idiot-proof interrfaces ;)

Comment 8 by David Parker posted on 4/5/2015 at 8:53 PM

What about adding button events to an intermediate array? Then you can observe that and pop the 1st or successful result off the "stack"

Fast and cheap. Is that too simplistic?

Comment 9 by Šime Vidas posted on 4/6/2015 at 2:39 AM

You can use the XHR .readyState property instead of the active variable:


if (xhr && xhr.readyState < 4) {

­čśü

During a typical $.getJSON call, you get a bunch of 1s and then a single 4 at the end: http://jsbin.com/wayake/edi...

Comment 10 (In reply to #9) by Raymond Camden posted on 4/6/2015 at 1:00 PM

You know I tried that - and oddly it didn't seem consistent for me - but I was probably just doing it wrong.

Comment 11 (In reply to #8) by Raymond Camden posted on 4/6/2015 at 1:01 PM

That seems more complex than simplistic. :) But I'd like to see that - can you share a JSBin?

Comment 12 (In reply to #7) by Raymond Camden posted on 4/6/2015 at 1:01 PM

That wouldn't help the main issue though. If the 1st click takes 3 seconds to work, and the second takes 0.5 seconds, we'd still have the same problem.

Comment 13 (In reply to #6) by Raymond Camden posted on 4/6/2015 at 1:03 PM

Pretty cool!

Comment 14 (In reply to #12) by Piotr z blog.piotrnalepa.pl posted on 4/6/2015 at 4:03 PM

I don't agree. I believe that after such a long time (3 sec) a user might want to see diferrent results so it shouldn't be an issue if the AJAX responses are quick.

Comment 15 (In reply to #14) by Raymond Camden posted on 4/6/2015 at 5:02 PM

But that's the problem I'm trying to solve here. User clicks 1, gets tired of waiting, clicks 2, sees the result, then sees 1's result come in, which imo is bad.

Comment 16 (In reply to #15) by Dumitru "Mitic─â" Ungureanu posted on 4/7/2015 at 10:03 AM

If an app is permitting several requests for the same token without checking for a response, then it creates the premise for DoS attacks.

Hence, I'd say you're trying to solve a problem that shouldn't exist in the first place.

Otherwise, allowing the user to make several AJAX calls, each regarding different tokens, and the user getting the responses in a different order, that's probably not something you should have to synchronize.

Comment 17 (In reply to #16) by Raymond Camden posted on 4/7/2015 at 1:03 PM

I wouldn't say this causes an opening for DoS. I mean shoot, even in my "fixed" version, all I need to do is copy the URL, open a new tab, and go crazy with reloads. By me fixing it, it has slowed down my time to get to the attack by about 30 seconds.

Comment 18 (In reply to #17) by Dumitru "Mitic─â" Ungureanu posted on 4/7/2015 at 6:59 PM

"I mean shoot, even in my "fixed" version, all I need to do is copy the URL, open a new tab, and go crazy with reloads."

It's something different. It's the equivalent of multiple users asking the same resource for the same token. It's normal operation mode. It has to do with the server load balance.

Your scenario, if I understood it correctly, is the equivalent of a single user being able to ask the same resource for the same token continuously. The fact that such a thing is possible in the first place, that's an issue. You should not have to "fix" this, "it" should not be possible to begin with.

Finally, the same user being able to ask the same resource for multiple tokens, that's also normal operation mode, as long as one token is fully resolved, one way or the other, before being able to ask for it again.

My point is, you're approaching this as a enhancement, I think it should be a requirement.

Comment 19 (In reply to #18) by Raymond Camden posted on 4/7/2015 at 7:15 PM

Maybe I don't understand you then. Given that URL X represents "info on product 1", how do you propose to stop me from requesting that N times a second? You can't - unless you do something on the server to block my traffic.

Comment 20 (In reply to #19) by Dumitru "Mitic─â" Ungureanu posted on 4/7/2015 at 7:30 PM

Exactly my point: server and client mechanisms to stop unsatisfied requests from being issued over and over again under the same identical parameters are not optional, these are solid requirements from the get go.

Comment 21 (In reply to #20) by Raymond Camden posted on 4/7/2015 at 7:33 PM

I still don't think I'm understanding you. Are you saying you shouldn't do something on the client to provide a better experience because it doesn't stop a potentially DDOS attack? To me - that doesn't make sense. I think the changes described above help provide a better user experience, which is a good thing. Do they prevent a DDOS? No. Does this blog entry discuss DDOS? No. Is that an important topic? Sure - but it isn't relevant to this entry nor does it imply that what I propose here shouldn't be done. Imo, it should.

Comment 22 (In reply to #21) by Dumitru "Mitic─â" Ungureanu posted on 4/7/2015 at 7:45 PM

Let's recap.

You start by proposing a user can make parallel requests and get responses in a different order. For me this is perfectly fine, if we're talking about different tokens. Do you see an issue so far?

The point where I agree is when you identify a problem with the user (EDIT: user session ) being able to ask twice or more times for the same token regardless of the response loop back.

The point where I disagree is that this problem should not exist, to be fixed, in production. Beside possibly circumventing things like data correctness for the user, it's also a DoS vector.

Comment 23 (In reply to #22) by Raymond Camden posted on 4/7/2015 at 7:50 PM

But the whole point of this article is that the user is requesting -2- different URLs. It's one service, but its two different pieces of data. And the problem is - how do you ensure that if they quickly click twice, they don't see a result that may be confusing. ("I hit product 2 last, but I see product 1s info.") They aren't asking for the same token in my example, but rather 2 different ones.

Comment 24 (In reply to #23) by Dumitru "Mitic─â" Ungureanu posted on 4/7/2015 at 7:52 PM

It's only confusing if each response is not properly formulated, otherwise it doesn't really matter which response gets faster. Product 1 and product 2 have any relationship or dependence? If yes, the user should not be able to request but in a dependent manner.

Comment 25 (In reply to #24) by Raymond Camden posted on 4/7/2015 at 7:56 PM

Then I believe we will have to agree to disagree on this point.

Comment 26 by Ravi posted on 4/22/2015 at 8:12 AM

Thanks Ray for the write up and your time. I have question say i have multiple async ajax request i make at the same time. how to handle such multiple request ?

Comment 27 (In reply to #26) by Raymond Camden posted on 4/22/2015 at 12:22 PM

I'd look into promises. If you go to my YouTube channel and view my jQuery tutorial series, I have one on just that topic.

Comment 28 by Dave Whit posted on 9/17/2015 at 9:57 PM

I realize this is an old post but thought I'd chime in. This seems like a perfect case for async.series why go through all this trouble?

Comment 29 (In reply to #28) by Raymond Camden posted on 9/17/2015 at 9:58 PM

Can you talk about what this is and how it would act?

Comment 30 by pradeep chinwan posted on 10/2/2015 at 11:38 AM

i'm stuck on this problem last 2 days....finally got it.........!!!!!!!!!!!!!!!!!!!!!

thanks a lot

Comment 31 by rWb posted on 10/15/2015 at 12:36 PM

I have written - when I was still very inexperienced in JS - an async queue manager that adds functions calls and ajax calls to a queue and runs them one-by-one and queue-after-queue to handle these cases with an added please wait animation. I don't do this that much lately as I found there is nothing wrong about getting data back in the wrong order and solved dependencies with deffered objects which are also good for animations by the way.

Anyway code is here without the fluff I have around it: http://jsfiddle.net/rawbits...
Not runnable in the fiddle though but perfectly working - at least I don't found any bugs.

Comment 32 by dmitrizzle posted on 4/9/2016 at 9:11 AM

I have a related question. My application loads bits of JS and JSON as required using AJAX. Occasionally though it would make a request to the same or same series of files. Ideally I don't want to do a server request for something that's in the memory already (files can be assumed to be static). What would be the best way you'd recommend I control for that?

One (important) thing to keep in mind is that the way my scripts are built is to wait for the files to load and then perform a series of tasks within onComplete{}. Is the only way to do this is to store variables that count whatever's been loaded? Or is there another way that would mean a lot less code restructuring for me?

Comment 33 (In reply to #32) by Raymond Camden posted on 4/10/2016 at 2:42 PM

I'd look into client-side storage. Modern browsers provide multiple ways to cache things in RAM/disk and let you fetch them later. I even wrote a book on it. :) (If you go to the About page here, you can see a link both to the book and to the video series I have on the topic.)

Comment 34 by Subba posted on 4/19/2016 at 3:44 PM

My Application , am calling a single ajax call when i call it loads the json file and when i select field in that it loads another json , here also one more link to json it also loading , here am increasing to pass one application to another application , ajax calls increasing and background also calling with each request , number of background pages increasing constantly

Comment 35 (In reply to #34) by Raymond Camden posted on 4/19/2016 at 3:56 PM

Ok - is that a question?

Comment 36 by ezion posted on 8/18/2016 at 11:13 PM

I ran into this same problem but came to a different solution than the ones mentioned so far. Though its similar to the send a token to and fro method mentioned by Ryan Guill below.
Extending object prototypes is a no no in JS. However extending instances is OK, as long you avoid name collisions.
By adding a myToken property to the XMLHttpRequest object instance you can see if the response handler is dealing with the request you want. If not, either ignore the result or when readyState < 4 abort() the request.

in code:

var LastEvent ; // this has to be global

function sendRequest(php, data) {
....var xhttp=new XMLHttpRequest();
........if (xhttp){
............xhttp.open("POST",php);
............xhttp.onreadystatechange=handleResponse;

............xhttp.myToken=getToken(); // get a unique identifier
............LastEvent=xhttp.myToken; // and remember the last one

............xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
............xhttp.send(data);
........}else alert("Get a better browser");
}

function handleResponse(e){

....if(LastEvent==this.myToken){ // Is this the response we want?

.........if (this.readyState==4){
.............if (this.status==200){
.................alert(this.responseText); // Do something with the result

.............}else alert(this.responseText); // Or show what went wrong

.........}else this.abort(); // Get rid of this request
....}
....e.stopPropagation();
}

Comment 37 by Brice Bentler posted on 2/19/2017 at 5:36 AM

Extremely helpful. Thanks!

Comment 38 by Harsha Vardhan posted on 3/9/2018 at 7:57 AM

Thanks a lot for sharing !! I

Comment 39 by billy posted on 3/20/2018 at 9:44 PM

Thank you!

Comment 40 by meric posted on 4/11/2018 at 12:49 PM

Hi Raymond, I'm trying to use your code, but I couldn't get it done. I'm trying to run a different query.php with the click of each button. I tried several ways, but couldn't make it. Something like this? <button data-prodid="query1.php" class="loadButton">Load One</button> but it didn't work. how can I do that using your code? please. thanks.

Comment 41 (In reply to #40) by Raymond Camden posted on 4/11/2018 at 1:02 PM

The data-prodid attribute is storing an ID value that gets read and made part of the request. But the request is to a hard coded ColdFusion file. You want to change that to your PHP and keep prodid as ID values.

Comment 42 (In reply to #41) by meric posted on 4/11/2018 at 1:12 PM

yes, I wanted to run an sql query with the button click. I have several buttons, and actually have a working code for this, but it is very messy and long with repetitions.

Comment 43 (In reply to #42) by Raymond Camden posted on 4/11/2018 at 1:32 PM

Cool... so you got it? I can't tell by your comment if you got it working.

Comment 44 (In reply to #43) by meric posted on 4/11/2018 at 1:35 PM

No, unfortunately, I couldn't use your code for this. I came across your page when I was looking for a simpler code to replace my existing code. At the moment, i use this code, and for every button, i repeat it. :

<script type="text/javascript">
function loadXMLDoc1()
{
var xmlhttp;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
else
{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("querydiv").innerHTML=xmlhttp.responseText;
}
}
xmlhttp.open("GET","query1.php",true);
xmlhttp.send();

}
</script>

Comment 45 (In reply to #44) by Raymond Camden posted on 4/11/2018 at 1:59 PM

Ok... but I explained how to fix the code though. You were using the URL in data-prodid, which in my demo represented a value you wanted to pass in to the back end service. If you literally change my jQuery code to point to your PHP and change the prodid values to be IDs, it should work. Of course, that assumes your back end code is a similar demo to mine.

Comment 46 (In reply to #45) by meric posted on 4/11/2018 at 2:14 PM

Thanks, Raymond! I'll work on that. Have a great day!