Skip to content
Blog / Handle race conditions when running operations in Appwrite DB
• 5 min

Handle race conditions when running operations in Appwrite DB

Learn how race conditions can occur while running operations on your Appwrite database and how operators can help avoid them.

A race condition is a special condition where events that are supposed to occur in sequence do not happen in sequence due to external factors, such as latency. These conditions are not ideal, as they could lead to outdated and incorrect data. Race conditions also exist in databases. If you're performing database operations through your app, it could get scary really quickly.

In this article, we will examine how race conditions can infiltrate your apps and how to prevent them using the new database operators in Appwrite.

A look at race conditions

Let's take a look at an example of how race conditions can lead to outdated and invalid data. Concurrent requests can cause race conditions, where if many requests are initiated at a single point in time, some of them might fail to register because old values are fetched and operated upon multiple times due to concurrency.

Here's an example of how this would typically happen in an Appwrite database.

React
// Get the counter value
const counter = await tablesdb.getRow({
  databaseId: "test-db",
  tableId: "counters",
  rowId: "first",
});

// Increment the counter
await tablesdb.updateRow({
  databaseId: "test-db",
  tableId: "counters",
  rowId: "first",
  data: {
    value: counter.value + 1,
  },
});

In the above code block, if multiple requests are sent simultaneously, there is a chance that the counter remains the same across multiple requests, missing an update or updating completely incorrectly. I have created a script to demonstrate this. If you want to check it out, I have published it on my GitHub Repository.

Running the script on a database with a row ID first and default value being 0, I got the following output.

======================================================================
šŸ”“ Race Condition: Unsafe Read-Modify-Write
======================================================================

šŸ“„ Initial: 0
šŸš€ Running 25 concurrent increments...

──────────────────────────────────────────────────────────────────────
šŸ“ˆ Results
──────────────────────────────────────────────────────────────────────

āœ… Expected: 25
šŸ“Š Actual:   1
šŸ’„ Lost:     24 updates
šŸ” Race detected:                                                            
24 workers read value 0 simultaneously
======================================================================

As you can see, when 25 requests were fired simultaneously, only one request successfully registered the correct value in the database, while the others retained the old values. This can be disastrous in data-sensitive applications where every request must account towards a change in database values.

The solution

To combat this problem, we recently announced a set of DB operators that you can use, which would atomically operate on the server side, ensuring that operations in all requests are captured successfully.

Let's take a look at an example code snippet that shows an accurate representation of how you can increment the counter successfully at each request using DB operators.

React
// Increment the counter
await tablesdb.updateRow({
  databaseId: "test-db",
  tableId: "counters",
  rowId: "first",
  data: {
    value: Operator.increment(1),
  },
});

As you can already tell from the above code snippet, we no longer need to fetch the value from the DB, which was the main cause of the race condition occurring. Instead, we pass in Operator.increment(1) as value. Removing the getRow() call also saves bandwidth in this case, and could be a substantial improvement if you do operations like these on scale.

This ensures the increment logic is performed atomically on the server, instead of the application layer. Now, let's run the script again, but with the DB operator in use.

======================================================================
🟢 Safe Increment: Using Atomic Operations
======================================================================

šŸ“„ Initial: 0
šŸš€ Running 25 concurrent increments...

──────────────────────────────────────────────────────────────────────
šŸ“ˆ Results
──────────────────────────────────────────────────────────────────────

āœ… Expected: 25
šŸ“Š Actual:   25
šŸ’„ Lost:     0 updates

āœ… No race condition: All increments processed atomically
======================================================================

As you can see, all the concurrent requests now register the increment, and the final value is 25, which can be double-checked in the database. Feel free to run this test yourself to verify the outcome.

Build fast, scale faster

Backend infrastructure and web hosting built for developers who ship.

  • checkmark icon Start for free
  • checkmark icon Open source
  • checkmark icon Support for over 13 SDKs
  • checkmark icon Managed cloud solution

Wrapping up

While learning to code, developers often find handling increments or any such operations on the application layer much easier. However, these operations tend to break when race conditions are introduced on scale, just as we explored in this article. DB operators ensure that your operations don't break when your app reaches the masses.

More resources

Start building with Appwrite today

Get started