Handling Concurrent Requests in a RESTful API

Imagine the following scenario:

  1. User A requests resource 1 via a GET endpoint
  2. User B requests resource 1 via a GET endpoint
  3. User A makes changes on resource 1 and saves its changes via a PUT request
  4. User B makes changes on resource 1, on the same fields as user A, and saves its changes via a PUT request

Since users A and B both requested the same version of resource 1, you now have a problem, because the PUT request triggered by user B erased the changes made by user A, and chances are, user A is mad and wondering why his changes do not show up anymore, even though he swears that he did not forget to save. Does that scenario sound familiar? If so, read on.

Identifying the problem

The problem is very simple: users A and B requested a resource more or less at the same time, and got the same version of this resource. Then, they both made changes to the resource in parallel, and whoever saved last erased the changes of the other.

There are actually 2 possible ways to solve this issue: through what’s called pessimistic locking or optimistic locking.

Pessimistic vs Optimistic locking

Pessimistic locking means that as soon as a user starts making changes on a resource, he locks this resource. While this resource is locked, no one else can edit this resource. When the user is done with his changes, he unlocks the resource so that other users can retrieve the resource and make changes to it. In other words, pessimistic locking ensures that 2 users can not make changes on one resource at the same time.

Optimistic locking on the other hand, allows users to retrieve a resource in parallel, however it will not allow a user to erase a previous version of a resource. If we take the example scenario above, in steps 1 and 2, users A and B will retrieve resource 1 both in, say, version 38. When user A saves resource 1, it will send to the API the information that the version being saved is version 38. The API will then retrieve the resource from the database and compare the version in database to the one provided in the API call, and if they differ, it will not accept the save operation. When user A saves version 38 of the resource, the save will be accepted and the version number will be incremented, but when user B tries to save version 38, the resource in the database will be at version 39, and therefore the save will not be accepted, preventing user B from overwriting user A’s changes.

What should be implemented in a RESTful API ?

So what kind of locking should you implement in your RESTful API ? Pessimistic or optimistic locking ?

Even though implementing pessimistic locking is not impossible, because a RESTful API is stateless, I would not recommend it, as this would mean that your API will need to rely on client applications to unlock resources automatically if users forget to do so.

Imagine the following scenario: user A retrieves resource 1 and the client application automatically locks the resource as user A starts making changes on it. After making some changes on resource 1 and saving them, user A closes the application and goes on vacation for 2 weeks… Unless your client application somehow automatically unlocked resource 1, resource 1 is now locked and can not be edited by anyone until user A is back from vacation… Unless you want to make your users mad at you, I do not advise you to go this way.

In optimistic locking however, users can not block each other from editing the same resource, so the issue created by pessimistic locking can not happen. In the scenario described in the introduction of this article, user B will simply get an error message telling him that his changes can not be saved because a new version of the resource was created by user A in the meantime. It is then up to you to decide whether you want to try to resolve conflicts between the changes made by user A and the changes made by user B automatically (which can be a nightmare) or whether you just ask user B to redo his changes after retrieving the new version of the resource.

Note that, if for some reason, this “inconvenience” caused by optimistic locking is not acceptable for you, you can still implement pessimistic locking and set a lock expiration to, for example, automatically release a lock that was created 2 hours ago, and is therefore expired. Your client application will then have to reflect this expiration.

How to implement optimistic locking

The standard way to implement optimistic locking in a RESTful API is using the Etags and If-Match headers. When a resource is retrieved, it is received with an Etag header, which is a string of characters identifying the version of the resource being retrieved. When a client later wants to update this resource, it has to provide the value of the Etag in the If-Match header. If the Etag provided in the If-Match header matches the version of the resource in the database, the resource can be updated, otherwise the update is rejected.

Note that if you are using MongoDB as a backend and Mongoose as an ORM, you already have a built-in version mechanism and do not need to implement one. Mongoose stores versions of a document in a __v attribute of a document. All you need to do is to make sure this __v attribute is incremented on every save operation. In order to do this, Mongoose provides a very convenient option: optimisticConcurrency (see documentation here), which, if set, will automatically increment this __v attribute on every save operation. Note however that it will not increment this attribute on a findOneAndUpdate operation, so you will need to do this manually:

await q.findOneAndUpdate({ _id }, {
$set: body,
$inc: { __v: 1 }
}

You will then need to provide this __v attribute on every GET request, either directly in the response payload or in the Etag header, and your client application will then need to send you this __v attribute, again either in the request payload or in the If-Match header.

When there is a conflict (a resource can not be saved because it was edited in the meantime), your API should reply with a 412 Precondition Failed HTTP status code. It will then be up to the client application to interpret this status code and to show a comprehensible error message to the user.

Conclusion: Implement Optimistic locking for your resources using the Etag and If-Match headers

Depending on your resources, you might not need to implement optimistic locking at all: if your resources are edited by a single user, there is no risk of concurrent requests and you will not need to worry about implementing optimistic locking.

If however some of your resources can be updated by different users, you will, sooner or later, encounter the scenario described at the beginning of this article. In this case, my recommendation is to implement optimistic locking using the Etag and If-Match headers, returning a 412 Precondition Failed if a resource can not be updated.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store