# How Yappuccino Handles Race Conditions in Django Voting with select_for_update()

> Learn how Yappuccino prevents race conditions in Django voting using select_for_update() and transaction.atomic() to lock vote records ensuring accurate counts under heavy traffic.

- Repository: [Ja'farbek Yusupov/yappuccino](https://github.com/jafarbekyusupov/yappuccino)
- Tags: internals
- Published: 2026-03-04

---

**The Yappuccino blog uses Django's `transaction.atomic()` wrapper combined with `select_for_update()` to acquire row-level locks on Vote records, preventing concurrent requests from corrupting vote counts even under heavy parallel traffic.**

The voting mechanism in the Yappuccino open-source blog platform must maintain accurate vote counts when multiple users interact with posts simultaneously. This article examines how the repository implements robust **race condition** protection using Django's database locking primitives within the `vote_post` view.

## The Race Condition Challenge in Voting Systems

When two users submit votes on the same post at the same millisecond, or when a single user double-clicks a vote button, the application risks creating duplicate records or calculating incorrect scores. Without proper locking mechanisms, concurrent database transactions can read stale data and overwrite each other's changes, leading to inconsistent `upvotes`, `downvotes`, and `score` values.

## How select_for_update() Prevents Concurrent Vote Corruption

The implementation in [`blog/views.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/views.py) follows a five-step locking strategy that forces concurrent vote operations to execute sequentially.

### Step 1: Wrapping Operations in Atomic Transactions

At line 531, the view opens an atomic transaction block using `with transaction.atomic():`. This ensures that all subsequent database operations—locking, reading, and writing—succeed or fail as a single unit. If any step fails, Django rolls back the entire transaction, leaving the database in its original consistent state.

### Step 2: Acquiring Row-Level Locks

Immediately inside the transaction at line 533, the code executes:

```python
curr_vote = Vote.objects.select_for_update().filter(post=post, user=user).first()

```

The `select_for_update()` method appends `FOR UPDATE` to the SQL query, acquiring a **row-level lock** on the existing vote record. This blocks any other transaction from reading or modifying that specific row until the current transaction commits or rolls back, effectively serializing concurrent vote requests targeting the same user-post combination.

### Step 3: Handling the Creation Race Window

Even with row locking, a microscopic race condition exists when creating new votes. If two requests simultaneously pass the `first()` check (finding no existing vote), both could attempt to insert a row. The code handles this at lines 547-557 by catching `IntegrityError`:

```python
try:
    Vote.objects.create(post=post, user=user, vote_type=vote_type)
except IntegrityError:
    # Re-read the vote that was just created by the concurrent transaction

    curr_vote = Vote.objects.select_for_update().filter(post=post, user=user).first()
    # Apply update logic to the existing vote...

```

This fallback guarantees correctness even when the database's unique constraint prevents the duplicate insertion.

## Complete Implementation in blog/views.py

The complete `vote_post` view implementation demonstrates the pattern in context:

```python
from django.db import transaction, IntegrityError
from django.shortcuts import get_object_or_404

def vote_post(request, pk, vote_type):
    post = get_object_or_404(Post, pk=pk)
    user = request.user

    with transaction.atomic():  # Line 531

        # Acquire lock on existing vote or None (Line 533)

        curr_vote = Vote.objects.select_for_update().filter(
            post=post, user=user
        ).first()

        if curr_vote:
            if curr_vote.vote_type == vote_type:
                curr_vote.delete()  # Toggle off

            else:
                curr_vote.vote_type = vote_type
                curr_vote.save()
        else:
            try:
                Vote.objects.create(
                    post=post, 
                    user=user, 
                    vote_type=vote_type
                )
            except IntegrityError:  # Lines 547-557

                # Concurrent insertion occurred; re-fetch and update

                curr_vote = Vote.objects.select_for_update().filter(
                    post=post, user=user
                ).first()
                if curr_vote and curr_vote.vote_type != vote_type:
                    curr_vote.vote_type = vote_type
                    curr_vote.save()

```

This approach ensures that the `post.score` calculated from these votes remains accurate regardless of request timing.

## Verifying Concurrency Safety

You can verify the locking behavior using threaded requests in a test environment:

```python
from threading import Thread
from django.test import Client

def simulate_concurrent_votes():
    client = Client()
    url = '/post/42/vote/upvote/'
    
    def vote():
        client.post(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
    
    threads = [Thread(target=vote) for _ in range(10)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    
    # Despite 10 simultaneous requests, only one vote record exists

```

Even with ten near-simultaneous requests targeting the same post and user, the database enforces sequential processing through `select_for_update()`, resulting in exactly one vote record rather than ten duplicates.

## Summary

- **Transaction atomicity** via `transaction.atomic()` groups all vote operations into indivisible units
- **Row-level locking** via `select_for_update()` (line 533) prevents concurrent reads of the same vote record
- **IntegrityError handling** (lines 547-557) catches race conditions during vote creation attempts
- The implementation lives in [`blog/views.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/views.py) within the `vote_post` function
- This pattern ensures accurate `upvotes`, `downvotes`, and `score` calculations under heavy load

## Frequently Asked Questions

### What happens if two users vote on the same post simultaneously?

Each user's vote operation runs in its own transaction with `select_for_update()` locking only the row specific to that user-post combination. Because the locks target different rows (assuming different users), both transactions proceed concurrently without blocking each other, maintaining high throughput while preventing individual user vote corruption.

### Why is select_for_update() necessary instead of just using get_or_create()?

The `get_or_create()` method is not atomic across its constituent get and create operations; two threads can simultaneously pass the "get" check and both attempt to create, causing duplicate entries or IntegrityError exceptions. The explicit `select_for_update()` lock forces the second thread to wait until the first thread completes, eliminating this window for duplicate creation.

### Does this locking impact application performance?

Row-level locks held only for the duration of the transaction microseconds typically impose minimal overhead for voting operations. The lock specifically targets individual vote rows rather than tables, allowing other posts and users to vote concurrently. Database engines like PostgreSQL and MySQL implement `FOR UPDATE` efficiently, making this approach suitable for high-traffic scenarios.

### Where is the Vote model defined in the repository?

The `Vote` model referenced in the locking queries is defined in [`blog/models.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/models.py). This model establishes the unique constraints on `(post, user)` pairs that enable the `IntegrityError` fallback mechanism to work correctly when `select_for_update()` alone cannot prevent the race condition during initial creation.