triadi's avatar
Level 1

Race Condition Issue in Laravel Controller

Context:

I'm encountering a race condition issue in my Laravel API. The API is used for student attendance tracking in a mobile app. The issue occurs when multiple requests for the same student and session are made simultaneously.

Controller Code:

/**
 * Store attendance record
 *
 * @param  AttendanceSaveRequest $request
 * @return JsonResponse
 */
public function store(AttendanceSaveRequest $request): JsonResponse
{
    info("starting transaction");
    DB::transaction(function () use ($request) {
        $attendance = Attendance::where('session_id', $request->session_id)
            ->where('student_id', $request->student_id)
            ->lockForUpdate() // 🔒 Prevent race condition
            ->first();

        if ($attendance) {
            throw new \Symfony\Component\HttpKernel\Exception\ConflictHttpException('Attendance already exists');
        }

        Attendance::create(array_merge($request->validated(), [
            'entry_user_id' => auth()->id(),
        ]));
    });

    $resource = (new AttendanceResource($attendance))
        ->additional(['info' => 'The attendance has been saved.']);

    return $resource->toResponse($request)->setStatusCode(201);
}

Database Table:

The attendances table has a composite primary key:

  • session_id

  • student_id

Issue:

When I test with high concurrency (e.g., using Postman Runner), I still get a Unique violation error:

SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "attendances_pkey"
DETAIL: Key (session_id, student_id)=(1799255, 2021010902) already exists.

usually in first 2 request.

my laravel.log look like this

SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "attendances_pkey"

starting transaction

starting transaction

Question:

Is my approach using lockForUpdate() incorrect? How can I properly prevent race conditions in this scenario? Any guidance is greatly appreciated!

1 like
8 replies
jlrdw's avatar

The issue occurs when multiple requests for the same student and session are made simultaneously.

How is multiple request happening in your code from the same student? Perhaps use a token.

For a single student you shouldn't be getting a race condition.

Edit:

Why do you need a lock, a lock is where two or more people could possibly edit a record at the same time.

1 like
triadi's avatar
Level 1

@jlrdw I’m not entirely sure how multiple requests are happening simultaneously for the same student. However, in production, I’m receiving multiple Sentry issue reports due to primary key constraint violations.

The API uses JWT middleware for authentication, so each request should be properly authenticated.

Would handling this with a try-catch block and returning an appropriate response be a good approach, or is there a better way to prevent this issue?

Snapey's avatar
Snapey
Best Answer
Level 122

Use upsert to add or update the record in a single atomic action. No need for transaction or locks.

1 like
jjoek's avatar

@Snapey I think considering that they are already experiencing race conditions, using upsert here might not be a good idea, as I know upsert to most times introduce database deadlocks. I think the best route here would have been as @jlrdw mentioned, find out how a single student is able to record attendances simultaneously, you could maybe add some sort of delay from the client's end between requests

Snapey's avatar

@jjoek what is it about upsert that might introduce deadlocks?

martinbean's avatar

@triadi Why are you manually converting the Eloquent API resource to a response and setting the status code? You‘re meant to just return the resource. It will automatically be converted to a response, and the status code will also be automatically set to 201 Created if the model was created in the current request:

- $resource = (new AttendanceResource($attendance))
-     ->additional(['info' => 'The attendance has been saved.']);
- 
- return $resource->toResponse($request)->setStatusCode(201);
+ return AttendanceResource::make($attendance)->additional(['info' => 'The attendance has been saved.'])
1 like
triadi's avatar
Level 1

@martinbean Thank you for the advice! That makes sense—I wasn’t aware that Eloquent API resources automatically handle the response conversion and status code. I’ll update my implementation accordingly.

1 like

Please or to participate in this conversation.