5.9. Race Conditions#

A race condition occurs when multiple processes or threads access and manipulate shared data simultaneously, leading to unexpected or incorrect behavior. This is particularly problematic in web applications where multiple users can send concurrent requests to modify the same database records.

5.9.1. Example#

Imagine a scenario where a website offers one free product, and two users attempt to claim it at the same time:

  • User 1 checks if the product is available (it is).

  • User 2 checks if the product is available (it is, because User 1 hasn’t claimed it yet).

  • Both users proceed to claim the product simultaneously.

  • Since both users saw the product as available before any update happened, they both successfully claim it - even though only one should have been allowed.

This happens because the operations “check if available” and “mark as claimed” are not atomic (i.e., they are separate steps and can be interrupted by another process).

5.9.2. Database Transactions#

A database transaction is a sequence of operations performed as a single unit of work. A transaction ensures that:

  • Atomicity – All operations within the transaction either succeed together or fail together (no partial updates).

  • Consistency – The database remains in a valid state before and after the transaction.

  • Isolation – Transactions do not interfere with each other, preventing race conditions.

  • Durability – Once a transaction is committed, the changes are permanently saved in the database.

This means that if two users try to claim the product at the same time, the database will only allow one of them to succeed.

5.9.3. SQLAlchemy Transactions and Flask#

When a route function interacts with the database, Flask-SQLAlchemy automatically manages transactions using db.session.

  • Every time a route function is called, a new db.session object is created and is accessible inside the route function.

  • To end the session, we call db.session.commit to attempt to make an atomic change to the database.

Sometimes the change can’t be made because of a conflict in the database. In such a case, an IntegrityError is raised, which we will need to handle. Normally we can just rollback the changes and show the user an error.

Using the example above, we might write our route function like this:

@app.route("/claim", methods=["POST"])
def claim_product():
    try:
        purchase = Purchase(product_id=product.id)
        db.session.add(purchase)
        db.session.commit()  # This commit will fail if another transaction already inserted the same product_id
        return "You've claimed the free product"

    except IntegrityError:  # Handles race condition conflicts
        db.session.rollback()
        return "Sorry something went wrong!"