← Back to the library

daily EDITION · EPISODE — · 9:01

Redis Locks: When the Old Request Wakes Up

A Redis lock can expire while its old holder still has a write left to send. What stops that request when it wakes up? Follow seat 14C from a concert-ticket checkout to the database condition that protects the booking. This new edition gives the story more room; the original episode remains available.

“Lock expired. Still running.” The Redis lock has expired while the old request for seat 14C is still running. Alex reacts in the Development Radio studio.

PRESS PLAY. PULL UP A CHAIR.

Watch the video edition on YouTube ↗

In this conversation

Maya and Alex follow one concert booking through a Redis lock that expires while its server is paused. They test the tempting fixes, put the booking condition inside the Postgres write, and ask whether Redis still earns its place. A new, more spacious conversation about Seat 14C.

TAKE IT INTO YOUR WEEK

One thing to try.

In an isolated test, pause one lock holder before its write until the lock expires; let another request commit, then resume the first and verify the authoritative write refuses the stale operation and the customer receives no false confirmation.

Follow the thinking

The original references behind this conversation.

Read the timed transcript

00:00:00 — Alex
Hey, welcome to Development Radio. I'm Alex, here with Maya. Today's story is about Redis locks. But we're going to arrive there through buying a concert ticket.

00:00:10 — Maya
That's a nice place to start. I'd be the person studying the seating plan, trying to find one perfect seat. Meanwhile, every perfectly good seat is being bought by somebody else.

00:00:21 — Alex
I'd choose something I could afford and close the seating plan before it talked me into an upgrade.

00:00:26 — Maya
Sensible. I'd call you while I was still deciding. Then you'd have to hear my argument for being three rows closer.

00:00:32 — Alex
I'd hear you out. You can make a surprisingly good case for three rows when it's somebody you really want to see.

00:00:39 — Maya
Once the booking is finished, though, I want to stop negotiating with the website. I've got my seat. Now I can look forward to the music.

00:00:46 — Alex
And that ordinary moment is our story. Someone chooses a seat and clicks Buy. Our app has to turn that into a booking they can trust.

00:00:55 — Maya
Let's use seat fourteen C. One person, one seat. I want to follow what happens behind that click.

00:01:01 — Alex
Let's start with the small version.

00:01:03 — Maya
Let's build a small ticketing app. Two servers can accept bookings. Both use the same Redis, and both save bookings in the same Postgres database.

00:01:12 — Alex
Postgres holds the seat record: who's booked fourteen C. Redis holds a separate lock for that seat. A server has to get the lock before it tries to book. If somebody else already has the lock, that request waits or backs off.

00:01:27 — Maya
And the lock expires after ten seconds. We need an expiry because a server might crash while holding it. Otherwise nobody else could get a turn.

00:01:35 — Alex
Right. Ten seconds is the limit we're choosing for this story. It gives a booking request time to work, but also gives the next request a way in if the first server disappears.

00:01:45 — Maya
Okay. I want to follow the first request before we bring in the second.

00:01:49 — Alex
Lena wants seat fourteen C. Her request goes to server A. A gets the Redis lock, then checks Postgres. The seat is free. But before A saves Lena's booking, the server pauses.

00:02:01 — Maya
So Lena hasn't got a seat yet. Her server checked, but it hasn't written anything.

00:02:07 — Alex
Exactly. Keep A there, paused before its write. The ten seconds pass. Redis expires the lock. A is still paused.

00:02:15 — Maya
The lock has ended. Lena's request hasn't ended with it.

00:02:19 — Alex
That's the gap. Redis removed its lock key. It didn't cancel the work waiting inside server A.

00:02:25 — Maya
Now let Omar try the same seat.

00:02:27 — Alex
Omar's request reaches server B. B can get a new Redis lock. It checks Postgres, and fourteen C is still free. Remember, Lena's server never saved her booking. B books the seat for Omar and commits that change.

00:02:41 — Maya
Omar has the seat now. If that were my confirmation email, I'd close the booking page. Done. I wouldn't keep checking whether the ticket was still mine.

00:02:49 — Alex
I'd probably leave the email open. I like seeing the confirmation.

00:02:53 — Maya
You can keep the tab. I'm done with it. Meanwhile, Lena's server is still paused.

00:02:59 — Alex
Yes. Omar's server finishes and releases its lock. Then A wakes up. Lena's server carries on from where it paused: just before saving her booking.

00:03:09 — Maya
It still has its old answer: the seat was free. It hasn't heard about Omar.

00:03:14 — Alex
And our original write doesn't check again. A overwrites the seat's buyer with Lena and sends her a confirmation. Now the database says Lena. Omar still has his email.

00:03:25 — Maya
There are our two promises. The second server didn't break into a lock. The first server simply kept going after its own lock was gone.

00:03:33 — Alex
Yes. We can make the pause twelve seconds. It might be a long garbage-collection pause. The important part is that it lasts longer than the ten-second lock. A resumes with work still to do.

00:03:45 — Maya
I want to hold onto that distinction. Redis knows the ten seconds have passed. But the code running on A doesn't get erased when Redis removes a key.

00:03:54 — Alex
And the next line of that code can still send a write to Postgres. That's why a live server isn't proof that its lock is still valid.

00:04:01 — Maya
My first repair would be another check. Before A writes, ask Redis whether the lock still belongs to A. In the story we just told, that would catch it.

00:04:11 — Alex
It would catch that pause. Move the pause a little later, though. A asks Redis and gets a yes. Then A stops, before the database write. The lock expires while A is stopped.

00:04:23 — Maya
And when A resumes, it's still carrying a yes from before the pause. I haven't joined the check to the write. I've only moved them closer together.

00:04:32 — Alex
Which can look reassuring in the code. The two lines are right beside each other. But the database receives the write later. It can't tell that the earlier Redis answer has gone stale.

00:04:43 — Maya
For the twelve-second pause we started with, a thirty-second lock would still be there when A wakes. I do want to acknowledge that. The longer expiry helps this request.

00:04:53 — Alex
It does. Lena's server would wake up with time left on its lock. Omar's request wouldn't have got in during those twelve seconds.

00:05:00 — Maya
But if we're choosing thirty because we need a guarantee, we still owe ourselves an answer about a longer pause. More time gives us margin. It doesn't put a limit on how long A can stop.

00:05:12 — Alex
And if A crashes, the next buyer waits longer for the lock to expire. We might accept that cost. We still need something that can refuse A's write after its time runs out.

00:05:23 — Maya
Before we leave Redis, our unlock code does guard against one late action. A can't just wake up and delete whichever lock happens to be there. We check that the stored random value is still A's.

00:05:35 — Alex
Yes, that stops A from deleting somebody else's lock. Keep it. But the harmful action we're following is a booking write. It goes to Postgres. Checking a value before deleting a Redis key doesn't make Postgres refuse that booking.

00:05:49 — Maya
So come back to the moment that matters. Omar's booking is already saved. Lena's server wakes up and tries to change the buyer to Lena. I want the database to say no to that write.

00:06:00 — Alex
Then put the condition in the write itself. Ask Postgres to set the buyer to Lena only if the buyer is still empty. That's one database operation. We aren't checking, returning to the application, and deciding to write afterwards.

00:06:15 — Maya
Let me run that against Omar's booking. The seat's buyer already says Omar. So the condition, buyer is empty, isn't true anymore.

00:06:23 — Alex
Right. Postgres leaves Omar's booking alone and reports that it updated zero rows. Zero matters here. It means Lena's request did not get the seat.

00:06:33 — Maya
I would want that result handled explicitly. No confirmation email just because the database call finished without an exception.

00:06:40 — Alex
Yes. A has to look at the result. Zero rows means tell Lena the seat was taken. Only a booking that committed successfully gets a confirmation. The database's refusal has to reach the customer.

00:06:53 — Maya
Lena still loses the seat, which feels lousy. But she finds out while she can choose another one. She doesn't learn it at the concert.

00:07:01 — Alex
And Omar keeps the seat he already booked. We haven't made the server pause disappear. We've made the late request unable to overwrite his booking.

00:07:09 — Maya
Which leaves me wondering whether I'd keep Redis here at all. If two requests reach that conditional update, the database still protects the seat. The lock isn't what makes the booking correct.

00:07:20 — Alex
I'd look at how much unwanted work the lock saves. Imagine a busy on-sale, with lots of people clicking fourteen C together. Redis can turn some requests away before they all reach the same database row.

00:07:33 — Maya
So keeping it is a load question. If this is a quiet ticketing app, I might prefer less machinery. If the same seat is getting hammered, the lock may earn its place.

00:07:44 — Alex
I agree. Either way, the condition stays on the booking write. We can discuss whether Redis is worth operating without making Omar's seat depend on that answer.

00:07:53 — Maya
Before I'd trust this repair, I want to see the pause happen on purpose. Sending two requests close together might never catch the awkward moment.

00:08:01 — Alex
In an isolated test, stop Lena's server right before its booking write. Leave it stopped until the Redis lock has expired. Now let Omar's request book fourteen C and commit. Only then let Lena's server continue.

00:08:16 — Maya
And don't stop at checking that one request failed. Look at the seat: Omar should still own it. Look at Lena's result: zero updated rows and no confirmation email.

00:08:26 — Alex
That gives us the whole path we were worried about. The old request wakes up, reaches the database, and is refused there. We're testing the refusal, not hoping the two requests arrive in a lucky order.

00:08:38 — Maya
For a lock in your own system, that's the question I'd take away: if its old holder resumes after expiry, what can still stop the write? Follow one request all the way to that answer.

00:08:50 — Alex
Back at the concert, Omar has fourteen C. Lena knows she needs another seat. The lock can expire, and the server can wake up late. We still make only one promise about that chair.

Download the transcript ↓