<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: srinu madhav vysyaraju</title>
    <description>The latest articles on Forem by srinu madhav vysyaraju (@srinu_madhavvysyaraju_b9).</description>
    <link>https://forem.com/srinu_madhavvysyaraju_b9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3779777%2F386d09ce-2e44-4b4c-8769-cf88c1ba69e9.jpg</url>
      <title>Forem: srinu madhav vysyaraju</title>
      <link>https://forem.com/srinu_madhavvysyaraju_b9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/srinu_madhavvysyaraju_b9"/>
    <language>en</language>
    <item>
      <title>Connection Storms and Orphan Locks: Why Your Lambda Outage Wasn't About Lambda</title>
      <dc:creator>srinu madhav vysyaraju</dc:creator>
      <pubDate>Tue, 05 May 2026 08:40:28 +0000</pubDate>
      <link>https://forem.com/srinu_madhavvysyaraju_b9/connection-storms-and-orphan-locks-why-your-lambda-outage-wasnt-about-lambda-2ohc</link>
      <guid>https://forem.com/srinu_madhavvysyaraju_b9/connection-storms-and-orphan-locks-why-your-lambda-outage-wasnt-about-lambda-2ohc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series recap.&lt;/strong&gt; &lt;a href="https://dev.to/srinu_madhavvysyaraju_b9/db-locking-101-for-serverless-devs-6n3"&gt;Post 1 — Locking 101 for Serverless Devs&lt;/a&gt; covered the mental model: pessimistic vs optimistic locking, what each isolation level actually locks, and why short-lived functions amplify every locking pain point. Worth reading first — this post leans on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In post 1, a thousand Lambdas queued on a single inventory row. That's the obvious failure mode — the kind you can almost see coming if you squint. Post 2 is about the one that actually wakes you up, because nothing in your dashboard says the word "lock":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda invocations: spiking, then flat-lining at the concurrency ceiling.&lt;/li&gt;
&lt;li&gt;Database CPU: 8%.&lt;/li&gt;
&lt;li&gt;Database connections: pinned at max.&lt;/li&gt;
&lt;li&gt;p99: timing out.&lt;/li&gt;
&lt;li&gt;Error rate: 100% on bookings, partial everywhere else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You restart the database. It works for ninety glorious seconds. Then it breaks again, the same way.&lt;/p&gt;

&lt;p&gt;That's a connection storm meeting an orphan lock. The lock is the actual cause. The connection storm is what turns it into a public incident with a postmortem doc attached. Let's walk through how the two combine, and the small set of settings that make this whole class of outage go away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A passenger taps "Book" on a popular flight. Your Lambda runs something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bookSeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paymentToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE flight_inventory
       SET seats_available = seats_available - 1
       WHERE flight_id = $1 AND fare_class = $2 AND seats_available &amp;gt; 0`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Charge the card before we commit. Seems safe — if charge fails, we roll back.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO bookings (id, flight_id, fare_class, passenger_id, charge_id)
       VALUES (gen_random_uuid(), $1, $2, $3, $4)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop here for a second and look at the shape of it. Between &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt; there's a network call to a third party. The &lt;code&gt;UPDATE&lt;/code&gt; already grabbed an exclusive row lock on the inventory row, and that lock is going to be held for as long as the payment call takes — every millisecond of it.&lt;/p&gt;

&lt;p&gt;On a normal day, payment is 300ms and nobody notices. On a bad day — provider is degraded, packet loss, customer fumbling a 3DS challenge — payment is 25 seconds. Your Lambda's timeout is 30 seconds, so it deadlines, the runtime kills the container, and the connection back to Postgres is left holding the bag.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "orphaned" actually means
&lt;/h2&gt;

&lt;p&gt;When a Lambda gets killed mid-transaction, three things happen, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Node process is terminated. The &lt;code&gt;pg&lt;/code&gt; client never gets a chance to send &lt;code&gt;ROLLBACK&lt;/code&gt;. It was about to. It just didn't.&lt;/li&gt;
&lt;li&gt;The TCP connection eventually times out at the OS level. This can take minutes, depending on your network defaults. Postgres has no idea anything is wrong yet.&lt;/li&gt;
&lt;li&gt;In the meantime, Postgres still sees a perfectly valid connection sitting in &lt;code&gt;idle in transaction&lt;/code&gt; state, holding every lock the transaction had acquired.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5str44vumxgnz2awsg7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5str44vumxgnz2awsg7c.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a booking platform, that means the inventory row for the popular flight is locked. By a transaction that will never commit. On a connection that no longer has a process behind it.&lt;/p&gt;

&lt;p&gt;Now multiply. Every other booking attempt for that flight runs &lt;code&gt;UPDATE&lt;/code&gt;, hits the lock, and waits. Lambda concurrency climbs. The connection pool drains. New invocations can't even get a connection. Your platform's retry behavior makes it worse — API Gateway retries, EventBridge retries, the front-end retries — and you're now spending compute creating &lt;em&gt;more&lt;/em&gt; doomed transactions to stack behind the dead one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ks0mh45aptrfvis3uz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ks0mh45aptrfvis3uz3.png" alt=" " width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The funniest part (you won't think it's funny at the time) is that database CPU stays at 8% throughout. Nobody's doing any work. They're all waiting on a row lock held by a ghost.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't fix the upstream. You can fix the blast radius.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fag0rt0is4mgvrp2e6bpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fag0rt0is4mgvrp2e6bpd.png" alt=" " width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Payment providers occasionally get slow. Fraud APIs occasionally get slow. Customers occasionally walk into an elevator mid-checkout. None of that is going away. What you &lt;em&gt;can&lt;/em&gt; do is make sure a slow upstream doesn't escalate from "some users see a hiccup" to "the whole booking flow is down."&lt;/p&gt;

&lt;p&gt;There are four knobs. Turn all of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;statement_timeout&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Caps how long a single SQL statement can run. If your &lt;code&gt;UPDATE&lt;/code&gt; ever takes longer than this, Postgres aborts it and returns an error. Easy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;statement_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 5 seconds — generous for OLTP&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also scope it tighter for specific transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL statement_timeout = '2s'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches runaway queries. It does &lt;em&gt;not&lt;/em&gt; catch a transaction where each individual statement is fast but the transaction as a whole is slow because something in the middle is sleeping on a network call. Which is — annoyingly — exactly the case we just walked through. So:&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;idle_in_transaction_session_timeout&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you take one thing from this post, take this one.&lt;/p&gt;

&lt;p&gt;It caps how long a connection is allowed to sit in &lt;code&gt;idle in transaction&lt;/code&gt; state before Postgres forcibly aborts the transaction and closes the connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;statement_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;idle_in_transaction_session_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 10 seconds&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this set, a Lambda that dies mid-payment causes Postgres to release the inventory row lock 10 seconds later. No human in the loop. No database restart. Without it, the lock can be held for the TCP timeout duration, which on default AWS networking you can measure in minutes — long enough for the entire booking flow to stay broken until somebody notices.&lt;/p&gt;

&lt;p&gt;This is the single most important Postgres setting for serverless workloads, and it ships disabled by default. Fix that.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;lock_timeout&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Caps how long a statement waits &lt;em&gt;for&lt;/em&gt; a lock before giving up. Without it, a transaction that hits a held lock waits forever — well, until &lt;code&gt;statement_timeout&lt;/code&gt; cuts it off. With it, you fail fast and surface the contention to the caller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL lock_timeout = '500ms'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a booking flow this is almost always what you want. If you can't get the inventory lock in 500ms, the user has already mentally moved on, and you'd rather return a clean retryable error than tie up a connection slot for four more seconds while they stare at a spinner.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Concurrency caps at the runtime
&lt;/h3&gt;

&lt;p&gt;The first three settings stop one bad transaction from holding a lock forever. They don't stop your fleet from creating bad transactions faster than the database can shed them.&lt;/p&gt;

&lt;p&gt;That's what reserved concurrency is for. Configure your booking Lambda with reserved concurrency = 200, and at most 200 of them can be running at once. Invocations beyond that get throttled at the platform layer, before they ever touch a connection. The retry storm has a ceiling. Search, check-in, and status keep working because they're not sharing the booking budget.&lt;/p&gt;

&lt;p&gt;The lever has different names depending on where you run — Vercel calls it concurrency limits, Cloudflare Workers has per-Worker concurrency — but the shape is identical. Cap blast radius at the runtime, not at the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short word on poolers
&lt;/h2&gt;

&lt;p&gt;You can't run a thousand raw Postgres connections, and if you try, the lock-table contention you're trying to fix gets &lt;em&gt;worse&lt;/em&gt;. Postgres is happiest in the low hundreds.&lt;/p&gt;

&lt;p&gt;So you put a pooler in front: RDS Proxy, PgBouncer, Supabase's pooler, Neon's, whichever fits. They multiplex many client connections onto fewer server connections. Two modes matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session pooling&lt;/strong&gt; — a client connection is mapped to a server connection for its lifetime. Behaves like raw Postgres. Doesn't really help in serverless, because every Lambda invocation is a fresh client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction pooling&lt;/strong&gt; — a server connection is rented to a client only for the duration of a transaction, then returned to the pool. This is what you want. A thousand Lambdas can comfortably share fifty server connections as long as no single transaction monopolizes one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Transaction pooling has sharp edges. They all share one root cause, so let's name it first, and then the four problems will basically write themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The root cause.&lt;/strong&gt; When you talk to Postgres directly, you have one connection and you keep it for the whole life of your client. Anything you set on it — settings, prepared queries, locks, subscriptions — stays put because you're the only one using it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg0ghyhgfioko52gpom6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg0ghyhgfioko52gpom6.png" alt=" " width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Transaction pooling breaks that assumption. Between each transaction, your "connection" is returned to the pool and can be handed to someone else. Your next transaction gets a &lt;em&gt;different&lt;/em&gt; server connection. Anything you stored on the old connection is no longer reachable from where you're standing.&lt;/p&gt;

&lt;p&gt;Every pitfall below is a variation on the same theme: a feature that quietly stores state on the server connection, used by code that doesn't realize the connection identity has changed underneath it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prepared statements
&lt;/h3&gt;

&lt;p&gt;A prepared statement is Postgres caching a parsed and planned query under a name, so re-running it is faster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;PREPARE&lt;/span&gt; &lt;span class="n"&gt;get_seat&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flight_inventory&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;flight_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;get_seat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'NYC-LON-FRI'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache lives on the &lt;em&gt;server connection&lt;/em&gt;. Many client libraries — including &lt;code&gt;pg&lt;/code&gt; for Node — quietly use prepared statements under the hood for performance, even when you didn't write &lt;code&gt;PREPARE&lt;/code&gt; yourself.&lt;/p&gt;

&lt;p&gt;What breaks under transaction pooling: your client prepares &lt;code&gt;get_seat&lt;/code&gt; on server connection A. Your next query lands on server connection B, which has never heard of &lt;code&gt;get_seat&lt;/code&gt;. Postgres responds with &lt;code&gt;prepared statement "get_seat" does not exist&lt;/code&gt;. Your code looks correct. The error is confusing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; tell your pooler or your client not to use prepared statements. PgBouncer in transaction mode requires this. For &lt;code&gt;pg&lt;/code&gt; in Node, set &lt;code&gt;?options=-c%20plan_cache_mode%3Dforce_custom_plan&lt;/code&gt; on the connection string, or pass a connection flag your pooler documents (RDS Proxy, Supabase, and Neon each have their own switch). The performance hit is real but small.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;SET&lt;/code&gt; without &lt;code&gt;LOCAL&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You can change Postgres settings on the fly. There are two flavors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- session-scoped: lasts until you disconnect&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UTC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- transaction-scoped: lasts until COMMIT/ROLLBACK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;LOCAL&lt;/code&gt;, the change persists on the server connection forever. Under direct connections that's fine. Under transaction pooling, your transaction commits, the connection goes back to the pool with &lt;code&gt;timezone = 'UTC'&lt;/code&gt; still applied, and the next tenant rents that connection and starts seeing UTC instead of whatever they expected. Their bug doesn't look like yours, and good luck tracing it.&lt;/p&gt;

&lt;p&gt;This applies to everything settable: &lt;code&gt;timezone&lt;/code&gt;, &lt;code&gt;search_path&lt;/code&gt;, &lt;code&gt;statement_timeout&lt;/code&gt;, role-related settings, all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; always use &lt;code&gt;SET LOCAL&lt;/code&gt; inside a transaction. The setting unwinds automatically at commit and you can't poison the pool.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Session-level advisory locks
&lt;/h3&gt;

&lt;p&gt;Postgres has application-level locks you can use as a lightweight mutex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_advisory_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;-- session-scoped: held until you unlock or disconnect&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_advisory_xact_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- transaction-scoped: held until COMMIT/ROLLBACK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session-scoped one is tied to a server connection. Under transaction pooling, the same lifecycle problem appears, but the consequences are uglier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You acquire &lt;code&gt;pg_advisory_lock(42)&lt;/code&gt; inside a transaction.&lt;/li&gt;
&lt;li&gt;Transaction commits. Connection goes back to the pool, &lt;em&gt;still holding&lt;/em&gt; the advisory lock, because the lock outlives transactions.&lt;/li&gt;
&lt;li&gt;Next tenant rents that connection. They now hold "your" lock. They might release it (&lt;code&gt;pg_advisory_unlock_all&lt;/code&gt; is a real footgun here). Or they might just block on their own work because something they didn't acquire is held.&lt;/li&gt;
&lt;li&gt;Meanwhile, the next time &lt;em&gt;you&lt;/em&gt; try to acquire the same lock, you'll be on a different server connection that doesn't have it, so two of you might "hold" lock 42 at the same time. The whole point of the lock just evaporated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frazu5b7huv9odxuzeud8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frazu5b7huv9odxuzeud8.png" alt=" " width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use &lt;code&gt;pg_advisory_xact_lock(key)&lt;/code&gt;. It releases automatically when the transaction ends, no matter what happens to the connection afterwards. There's almost never a reason to use the session-scoped variant in serverless code.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;LISTEN&lt;/code&gt; / &lt;code&gt;NOTIFY&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Postgres has a built-in pub/sub. Your client runs &lt;code&gt;LISTEN bookings&lt;/code&gt; once, and any process that calls &lt;code&gt;NOTIFY bookings, 'something happened'&lt;/code&gt; causes Postgres to push a message to your connection.&lt;/p&gt;

&lt;p&gt;The subscription is bound to the server connection. Under transaction pooling, your connection identity changes constantly, so you might &lt;code&gt;LISTEN&lt;/code&gt; on connection A, leave the transaction, get connection B next time, and have no idea where the notifications are going. They might be queuing on a connection some other tenant is using. They might be silently dropped. Worst of all, sometimes it works and sometimes it doesn't, depending on which connection you happen to draw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; don't run &lt;code&gt;LISTEN&lt;/code&gt; / &lt;code&gt;NOTIFY&lt;/code&gt; through a transaction pooler. If you need it, open a dedicated long-lived connection that bypasses the pooler (most managed Postgres services expose a separate endpoint for this), or use a real message broker — Redis, SQS, NATS — for pub/sub.&lt;/p&gt;

&lt;p&gt;The shorter version of this whole section: in transaction pooling, the connection you talked to last time is not the connection you're talking to now. Anything that relied on "it's the same connection" needs a different design.&lt;/p&gt;

&lt;p&gt;And one thing a pooler does &lt;em&gt;not&lt;/em&gt; do is rescue you from orphan locks. If a Lambda hangs mid-transaction, the rented server connection sits in &lt;code&gt;idle in transaction&lt;/code&gt;, the row lock is still held, and the queue still forms. The pooler just keeps you from running out of connection slots while it happens. The four settings above are still doing the real work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up
&lt;/h2&gt;

&lt;p&gt;Connection setup for a serverless booking service, all the pieces in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// points at RDS Proxy or PgBouncer&lt;/span&gt;
  &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// one connection per Lambda container; the pooler does the multiplexing&lt;/span&gt;
  &lt;span class="na"&gt;statement_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;idle_in_transaction_session_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;query_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// client-side belt to the server-side suspenders&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bookSeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;paymentToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Charge the card BEFORE opening the transaction.&lt;/span&gt;
  &lt;span class="c1"&gt;//    The charge call now holds no DB resources.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Short, local-only transaction.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL lock_timeout = '500ms'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE flight_inventory
       SET seats_available = seats_available - 1
       WHERE flight_id = $1 AND fare_class = $2 AND seats_available &amp;gt; 0
       RETURNING seats_available`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Roll back AND refund — we charged but couldn't book.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sold out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO bookings (id, flight_id, fare_class, passenger_id, charge_id)
       VALUES (gen_random_uuid(), $1, $2, $3, $4)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better — but if you stare at it long enough you'll notice we've quietly traded one problem for another. If the Lambda dies &lt;em&gt;between&lt;/em&gt; the charge and the booking insert, the customer is charged with no seat. The lock is gone, which is great, but the money's gone too, which is less great.&lt;/p&gt;

&lt;p&gt;That's the exact gap &lt;strong&gt;idempotency keys&lt;/strong&gt; close, and it's the whole subject of post 3. Don't fix it today. Today's win is smaller and more valuable: ship the four settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can take to a code review
&lt;/h2&gt;

&lt;p&gt;Walk through these against whatever serverless Postgres setup you have. If any answer is "no," you're one slow upstream away from a bad afternoon.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is &lt;code&gt;idle_in_transaction_session_timeout&lt;/code&gt; set, on every connection or as a database-wide default?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;statement_timeout&lt;/code&gt; set, at a ceiling that matches your real OLTP workload?&lt;/li&gt;
&lt;li&gt;Are you using &lt;code&gt;lock_timeout&lt;/code&gt; inside transactions that touch hot rows?&lt;/li&gt;
&lt;li&gt;Does your function platform cap concurrency on the booking path so retry storms can't compound?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus round: are external calls (payment, fraud, PNR generation, anything that talks to the network) outside your transactions, not inside them?&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Post 3 picks up exactly where this one stops: how to make a booking flow safe to retry without holding any locks at all, using idempotency keys. That's the move that turns "Lambda died mid-flow" from an incident into a no-op.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>lambda</category>
      <category>postgres</category>
      <category>rds</category>
    </item>
    <item>
      <title>DB Locking 101 for Serverless Devs</title>
      <dc:creator>srinu madhav vysyaraju</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:17:54 +0000</pubDate>
      <link>https://forem.com/srinu_madhavvysyaraju_b9/db-locking-101-for-serverless-devs-6n3</link>
      <guid>https://forem.com/srinu_madhavvysyaraju_b9/db-locking-101-for-serverless-devs-6n3</guid>
      <description>&lt;p&gt;A flash sale goes live on a popular booking platform. Fifty dollars off the Friday evening flight from New York to London. Within seconds, a thousand Lambdas wake up and each one runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;flight_inventory&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;seats_available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seats_available&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;flight_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NYC-LON-FRI'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;fare_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your p99 latency jumps from 50ms to 30 seconds. No code changed. No deploy. CPU is fine. Connections look healthy-ish. What happened?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;locks&lt;/strong&gt;. If your mental model of database locking is fuzzy, serverless will find every gap and turn it into a pager alert. This post is the foundation: pessimistic vs optimistic, what isolation levels actually lock, and why short-lived functions amplify the pain.&lt;/p&gt;

&lt;p&gt;We'll use Postgres and TypeScript throughout, and the running example is a booking platform — flights, seats, fare classes — because most of the interesting concurrency problems show up there in their natural habitat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why locks exist
&lt;/h2&gt;

&lt;p&gt;When two transactions touch the same row at the same time. Without coordination, one overwrites the other and you get corruption: a seat sold twice, a hold counter that ticks once instead of twice, a fare class that goes negative. The database prevents this by serializing conflicting access using locks.&lt;/p&gt;

&lt;p&gt;A lock is a temporary, exclusive (or shared) claim on a piece of data, held by a transaction until it commits or rolls back. That's it. The complexity comes from which data, how exclusive, and for how long.&lt;/p&gt;

&lt;p&gt;Throughout this post, assume a schema like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;flight_inventory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;flight_id&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fare_class&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;seats_available&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seats_available&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt;         &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flight_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fare_class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flight_id&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fare_class&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;passenger_id&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;      &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pessimistic locking: claim it before you touch it
&lt;/h2&gt;

&lt;p&gt;Pessimistic locking assumes contention will happen and grabs a lock up front.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bookSeatPessimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Grab an exclusive row lock on the inventory row.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`SELECT seats_available
         FROM flight_inventory
         WHERE flight_id = $1 AND fare_class = $2
         FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flight not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;seats_available&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sold out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`UPDATE flight_inventory
         SET seats_available = seats_available - 1
         WHERE flight_id = $1 AND fare_class = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`INSERT INTO bookings (id, flight_id, fare_class, passenger_id)
         VALUES (gen_random_uuid(), $1, $2, $3)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SELECT ... FOR UPDATE&lt;/strong&gt; grabs a row-level exclusive lock. Any other transaction that tries to update or FOR UPDATE the same inventory row blocks until you commit. There are softer variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FOR SHARE&lt;/strong&gt; — read lock; multiple readers allowed, writers blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FOR UPDATE NOWAIT&lt;/strong&gt; — fail immediately instead of waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FOR UPDATE SKIP LOCKED&lt;/strong&gt; — pretend locked rows don't exist (the foundation of a Postgres job queue, covered in a later post — handy for ticket issuance workers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Felm7lhq5jei0f5pvmm6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Felm7lhq5jei0f5pvmm6p.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost of pessimistic locking:&lt;/strong&gt; while you hold the lock, every other booking attempt for that flight queues. If your function takes 200ms (because it also calls the payment provider, generates a PNR, and emits an event) and 500 invocations want the same flight, the last one waits 100 seconds. Deadlocks are also possible if two transactions grab locks in different orders — Postgres detects them and aborts one, but the retry is on you.&lt;/p&gt;

&lt;p&gt;For booking platforms, this gets worse fast: the row people fight over (popular flight, popular fare class, popular date) is also the row most likely to have a viral moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic locking: assume no contention, verify on write
&lt;/h2&gt;

&lt;p&gt;Optimistic locking skips the lock entirely. You read the row, do your work, and on write you check that nothing changed underneath you. The version column from the schema above does the bookkeeping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bookSeatOptimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`SELECT seats_available, version
           FROM flight_inventory
           WHERE flight_id = $1 AND fare_class = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flight not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;seats_available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seats_available&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sold out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`UPDATE flight_inventory
           SET seats_available = seats_available - 1, version = version + 1
           WHERE flight_id = $1 AND fare_class = $2 AND version = $3`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// someone else booked first; retry&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`INSERT INTO bookings (id, flight_id, fare_class, passenger_id)
           VALUES (gen_random_uuid(), $1, $2, $3)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flightId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;passengerId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too much contention, gave up after 3 retries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a concurrent transaction bumped the version between your read and your write, the UPDATE matches zero rows. You roll back and retry.&lt;/p&gt;

&lt;p&gt;Postgres also exposes a built-in version via the &lt;strong&gt;xmin&lt;/strong&gt; system column (the transaction id that last wrote the row). You can use it instead of an explicit version column, with the caveat that &lt;strong&gt;xmin&lt;/strong&gt; changes on every update including unrelated columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmbulg7xg8card0zn56j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmbulg7xg8card0zn56j.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost of optimistic locking:&lt;/strong&gt; under heavy contention, retries pile up. If 100 booking attempts all want the last seat in the same fare class, 99 of them lose the race, retry, lose again, and so on. It works beautifully when contention is rare and terribly when it isn't. For most flights on most days, it's the right default. For the flash-sale flight, it isn't — and we'll address that with sharding and holds in later posts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation levels:&lt;/strong&gt; what your transaction actually sees&lt;/p&gt;

&lt;p&gt;Isolation level controls which anomalies a transaction can observe. In Postgres:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Dirty read&lt;/th&gt;
&lt;th&gt;Non-repeatable read&lt;/th&gt;
&lt;th&gt;Phantom read&lt;/th&gt;
&lt;th&gt;Write skew&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed (default)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (snapshot)&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read Committed&lt;/strong&gt; locks a row only when you write it. Reads see the latest committed value at the moment of each statement. This is what you get by default, and what most apps run on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable Read&lt;/strong&gt; gives your transaction a consistent snapshot at the moment it began. Reads inside the transaction return the same data even if others commit. Postgres uses MVCC for this, no extra read locks needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serializable&lt;/strong&gt; uses predicate locking (SSI — serializable snapshot isolation) to detect any pattern of reads and writes that couldn't have happened in some serial order, and aborts one of the offenders. It's the only level that prevents write skew without manual FOR UPDATE.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write skew is worth pausing on, because booking platforms hit it often. Suppose policy says "always keep at least one seat in fare class Y reserved for elite upgrades." Two concurrent booking transactions each read seats_available = 2, each conclude "fine, plenty of buffer," and each book a seat. End state: seats_available = 0, constraint violated, no single transaction did anything wrong. Read Committed and Repeatable Read both allow this. Serializable does not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki4lpw4u62azsfrcqk1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki4lpw4u62azsfrcqk1z.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You set the level per transaction:&lt;br&gt;
&lt;code&gt;await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Serializable is correct-by-default but produces serialization failures (SQLSTATE 40001) under contention, which you must retry. Most production booking systems run Read Committed and add explicit locks (or unique-index-backed constraints) where needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why serverless makes all of this harder
&lt;/h2&gt;

&lt;p&gt;Now the serverless angle. The locking mechanics above are the same on a long-lived monolith. What changes is the runtime around them.&lt;/p&gt;

&lt;p&gt;Many short transactions instead of a few long ones. A monolith might handle 100 booking requests on one connection over a minute. Serverless spins up 100 concurrent functions, each with its own connection and its own micro-transactions. More acquire/release churn, more contention surface area.&lt;/p&gt;

&lt;p&gt;Connection counts explode. Lambda concurrency of 1000 means up to 1000 Postgres connections unless you put a pooler in front. Each idle connection still holds whatever locks its last transaction grabbed if you forgot to commit. (We'll fix that in post 2 of this series.)&lt;/p&gt;

&lt;p&gt;Retries are automatic and unsafe by default. API Gateway retries, Step Functions retries, EventBridge retries. If your function charged the card, then timed out before writing the booking row, the retry can charge again unless you've thought carefully about idempotency. (Post 3. Coming soon)&lt;/p&gt;

&lt;p&gt;Cold starts inside transactions. A Lambda that begins a transaction, calls the payment provider for 5 seconds, then commits, can hit its own timeout while holding row locks on the popular flight. The connection sits orphaned in idle in transaction state until Postgres reaps it, and during that window every other booking for that flight blocks behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3vs0vs2bm72mujdb5x0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3vs0vs2bm72mujdb5x0.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No shared memory. You can't reach for a process-local mutex or an in-memory cache to deduplicate work across invocations. The database (or Redis) is the only coordination point.&lt;/p&gt;

&lt;p&gt;The thread running through the rest of this series is simple: serverless hates long locks. Anything that holds a lock across a network call (payment, fraud check, PNR generation), a cold start, or a retry boundary is a future incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most CRUD on most tables, do nothing — Read Committed plus normal UPDATE statements handle it. When you need to coordinate concurrent writes to the same row (booking the same seat, decrementing the same counter), follow this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Default to optimistic locking with a version column and a small retry loop. It scales well, holds no locks, and degrades visibly (you see retries climb in logs) before it fails.&lt;/li&gt;
&lt;li&gt;Switch to pessimistic (SELECT ... FOR UPDATE) when you've measured high contention and retry cost is worse than queue cost — typically when the work inside the transaction is expensive or has external side effects you don't want to redo.&lt;/li&gt;
&lt;li&gt;Use SKIP LOCKED when you want queue semantics: many workers, each grab a different row. Ticket issuance and email-confirmation workers are textbook fits.&lt;/li&gt;
&lt;li&gt;Reach for Serializable only when write skew is a real risk (e.g., the "keep one seat for upgrades" policy) and you can't express the constraint as a unique index or a CHECK.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Back to the opening puzzle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A thousand Lambdas all run &lt;code&gt;UPDATE flight_inventory SET seats_available = seats_available - 1 WHERE flight_id = 'NYC-LON-FRI' AND fare_class = 'Y'&lt;/code&gt;. Even though no one wrote &lt;code&gt;FOR UPDATE&lt;/code&gt;, the UPDATE itself takes a row-level exclusive lock for the duration of the transaction. Concurrent updaters queue. With a thousand of them, the queue depth is a thousand, and your p99 is the time it takes for 999 transactions to finish before yours.&lt;/p&gt;

&lt;p&gt;Two fixes, both covered later in the series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the work idempotent and use optimistic retries, so contention surfaces as fast retries instead of long waits.&lt;/li&gt;
&lt;li&gt;Shard the hot row — keep N inventory rows per (flight, fare class) and pick one at random — so contention spreads across N queues. The booking layer reassembles the total before showing availability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In post 2, we'll get specific about the connection-storm-meets-orphan-lock failure mode (the payment-provider-hang case above) and how to defuse it with timeouts and a pooler.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>postgres</category>
      <category>database</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
