They Added Capacity and It Did Not Help: What WorkOS's Connection-Pool Starvation Says About Hold-and-Wait Failures
TL;DR: On July 16, 2026, WorkOS experienced a major service disruption caused by a database connection pool exhaustion incident where scaling up capacity failed to restore service. This "hold-and-wait" failure occurred when a nested transaction bug forced single requests to acquire two connections from a shared pool, leading to a self-sustaining deadlock even as the underlying database remained healthy.
Ninety minutes of ruling things out
The timeline of the WorkOS disruption on July 16, 2026, serves as a masterclass in the psychological toll of "ghost" outages. According to the WorkOS postmortem, impact began at 08:00 UTC. An incident was declared shortly thereafter, with the team officially mobilizing between 08:06 and 08:26.
For the next ninety minutes—from 08:26 until 09:48—responders followed the standard operational playbook. They ruled out recent deployments. They checked feature flag toggles. They analyzed traffic patterns to rule out volumetric attacks. They performed the one action every engineer is taught to do when a resource is saturated: they added capacity.
Yet, as the report explicitly states, service capacity was increased without improvement. In a database connection pool exhaustion incident, the typical reflex to scale horizontally often fails because the bottleneck is not the volume of requests, but the circular nature of the resource acquisition. Root cause was eventually isolated at 09:50, a fix to disable the problematic analytics write was deployed at 10:07, and the service recovered fully by 10:10.
The mechanism, stated plainly
The technical failure at the heart of this incident is a classic hold and wait deadlock production scenario. Under normal circumstances, an authentication request would enter the system, borrow one connection from the pool, perform its transaction, and return the connection.
However, a latent bug in the ORM (Object-Relational Mapping) layer changed this behavior for certain paths, specifically involving "AuthKit Analytics." Instead of reusing the existing, open connection for a nested transaction, the ORM was configured—or misinterpreted—to request a second connection from the same pool while still holding the first.
At low concurrency, this is an invisible performance tax. You use two connections instead of one, but the pool is deep enough that no one notices. However, once traffic spiked, the system hit a mathematical wall. If your pool has 100 connections and 51 requests simultaneously reach the "nested" stage, the first 50 requests will each hold one connection and wait indefinitely for a second. Because the pool is now empty, the 51st request cannot even start, and the first 50 cannot finish. Every connection in the pool is held by a request waiting for a connection that will never be released.
Why it stayed broken after the traffic stopped
One of the most frustrating aspects of a database connection pool exhaustion incident is its ability to self-sustain. You might expect that once the initial spike in traffic subsides, the pool would "drain" and return to health. This rarely happens in a hold-and-wait scenario.
Queued work, ongoing background tasks, and aggressive client retries act as a perpetual motion machine. As soon as a connection is released (perhaps due to a timeout), it is instantly snatched up by a new request that immediately enters the same "hold-and-wait" state.
Furthermore, a long connection borrow timeout extended the duration of the outage. If a request is willing to wait 30 seconds to get a connection from the pool, it sits in the queue, consuming memory and keeping the "wait" alive. This prevents the rapid "fail-fast" behavior necessary to break the cycle of starvation.
The signature
Identifying this failure class requires looking at three signals simultaneously. If you only look at one, you will likely head down a rabbit hole of unnecessary scaling.
- The pool is saturated: Your metrics show 100% connection utilization and high "borrow wait" times.
- The database is healthy: CPU is low, IOPS are normal, and locks at the engine level are absent. The database is literally waiting for work that never arrives.
- Scaling fails: You double the number of pods or the size of the pool, and the error rate remains flat. The new capacity is instantly consumed by the same circular logic.
Key Takeaway: If the pool is saturated, the database is healthy, and adding capacity does nothing, stop scaling and start looking for work that holds one resource while waiting for another of the same kind.
Where this lives in your codebase right now
This is not a "WorkOS bug"; it is a systemic risk in almost every modern back-end architecture. It specifically thrives in environments where workload isolation critical path principles are ignored.
The most common culprit is optional or "best-effort" work—like analytics, logging, or webhooks—wrapped inside a critical-path database transaction. If that optional work requires its own connection (a nested transaction second connection), you have created a landmine.
Common locations for this risk:
- ORM savepoints that emulated nested transactions by opening new handles.
- Audit log triggers that write to a separate table or schema using a different client configuration.
- Shared connection pools that serve both high-priority user authentication and low-priority background reporting.
Three fixes with different half-lives
To prevent a database connection pool exhaustion incident, you must address the defect, the architecture, and the containment strategies.
1. The Defect Fix (Short-term)
Ensure your ORM is configured to reuse the existing transaction context for nested calls. If you must use nested logic, use real SQL savepoints which operate within the same connection, rather than requesting a new one from the pool.
2. The Isolation Fix (Medium-term)
Move optional work out of the synchronous request path. Analytics should be buffered in memory or sent to a sidecar/message queue. Additionally, implement a "kill switch" (feature flag) that can disable these non-critical writes during an incident without requiring a full code deploy.
3. The Containment Fix (Long-term)
Lower your connection borrow timeout. It is better for a request to fail in 100ms and return its resources than to hang for 10 seconds and starve the rest of the system. Implement bounded concurrency at the application level to ensure no single type of request can consume more than, say, 50% of the total pool.
What a good postmortem looks like
According to the WorkOS blog, "Service disruption on July 16, 2026," a high-quality postmortem is defined by its transparency. WorkOS provided per-surface error rates and a detailed UTC timeline that included their dead ends—not just the eventual "eureka" moment.
They categorized their corrective actions into completed (e.g., stopping the analytics write), in-progress (e.g., upgrading pooled connection logic), and planned (e.g., architectural decoupling). This level of detail builds trust with engineering leaders who rely on their infrastructure for critical auth paths.
The audit worth running this week
To ensure your system is not vulnerable to a similar hold-and-wait failure, perform this four-step audit:
- Audit Nested Transactions: Grep your codebase for nested transactions or savepoints on critical paths. Verify if they acquire a new connection or reuse the existing one.
- Evaluate Shared Pools: List every workload sharing your primary database pool. Check if a spike in "reporting" or "background" work could starve your "login" or "payment" paths.
- Check Borrow Timeouts: Ensure your connection borrow timeout is significantly shorter than your global request timeout.
- Test Kill Switches: Confirm you have the runtime ability to disable optional writes (like analytics or third-party integrations) via a configuration change rather than a deploy.
According to Michael Grinich on X, the "trigger was an unusual increase in session-creation traffic," but the root cause was the latent bug. Your system will eventually hit a traffic spike; the goal of this audit is to ensure that spike doesn't turn into a ninety-minute mystery.
How Operate prevents the "Ninety-Minute Mystery"
The delay in resolving the WorkOS incident stemmed from the fact that the failure signature didn't match the traditional "just scale it" mental model. This is exactly why we built Operate.
Operate is designed to handle this specific class of incident by monitoring the entire stack—code, database, and logs—simultaneously. Instead of a human responder manually ruling out deployments and feature flags over ninety minutes, Operate's AI SRE detects the resource-contention signature in the shared pool immediately. It can identify the specific "hold-and-wait" pattern by correlating connection pool wait times with the specific lines of code in the ORM that are requesting dual handles.
Operate doesn't just alert you that the pool is full; it assembles the evidence, finds the root cause transaction, and can even draft the PR to move the analytics write out of the critical path. For teams that value control and privacy, Operate provides the auditability to see exactly why it reached its conclusion, letting you fix the issue in minutes, not hours.
Sources & further reading
- According to the WorkOS Blog, the service disruption was a result of database connection pool exhaustion triggered by a session-creation traffic spike.
- According to Michael Grinich on X, the root cause involved a latent transaction bug and insufficient workload isolation.
- For more on triage, see Is It Us or Is It AWS? A Triage Protocol for the First Fifteen Minutes.
- For more on system recovery, see Recovery Is Not Convergence.