It was a Thursday afternoon in Kigali, six days before the Hiddit app was scheduled to go live. A tester from the client side sent a message we dreaded: 'Two users booked the same court at the same time.' The receipt said both bookings were confirmed. That sent us down a three-night debugging spiral that changed how we think about concurrency for good.
The setup
Hiddit is a real-time sports court booking app. Players can see live court availability, pick a slot, and book — all in under 30 seconds. We had built the booking flow with optimistic locking: check availability, create a booking if the slot is free, return confirmation. Simple. Except under concurrent load, 'check then write' doesn't mean what you think it means.
At the same millisecond, two players checked court #4 at 6pm. Both queries returned 'available'. Both proceeded to write. PostgreSQL's default READ COMMITTED isolation level allowed both transactions to complete — and we had a double booking.
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError("")
setLoading(true)
try {
const data = await apiFetch<{ access_token: string }>("/auth/login", {
method: "POST",
body: JSON.stringify({ password }),
})
setToken(data.access_token)
router.push("/admin/dashboard")
} catch (err) {
setError(err instanceof Error ? err.message : "Invalid password.")
} finally {
setLoading(false)
}
}
Saad
Engineer at ASYNCC. Shipping software that works in the real world, not just in dev.