{"id":7871,"date":"2026-11-02T03:49:03","date_gmt":"2026-11-02T11:49:03","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sql\/?p=7871"},"modified":"2026-11-05T10:39:03","modified_gmt":"2026-11-05T18:39:03","slug":"your-server-wasnt-slow-it-was-waiting-we-fixed-it-with-constant-time-recovery","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sql\/your-server-wasnt-slow-it-was-waiting-we-fixed-it-with-constant-time-recovery\/","title":{"rendered":"The paper that explains how Microsoft SQL learned to recover without undoing everything first"},"content":{"rendered":"<p><span style=\"font-family: arial, helvetica, sans-serif;\"><em>Under the Hood: The Research Behind Microsoft SQL, part 2<\/em><\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">Here\u2019s a scenario that used to be genuinely frightening: you kick off a bulk load of a few hundred million rows in a single transaction. Somewhere in the middle, the server goes down. Not because of your load, just an unlucky reboot.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">When SQL Server comes back, it has to undo everything that transaction did before anyone can touch those tables. Row by row (I think I\u2019ve heard probably Bob Ward say \u201crow by agonizing row\u201d to describe this). Working backwards through the log.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">That exact thing happened to an Azure SQL customer, and the team wrote it into the opening of their paper:<\/span><\/p>\n<blockquote><p><span style=\"font-family: arial, helvetica, sans-serif;\">\u201cUnexpected failure occurred while one of our customers was attempting to load hundreds of millions of rows in a single transaction. The database required 12 hours to recover while the corresponding tables were completely inaccessible.\u201d<\/span><\/p><\/blockquote>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">Twelve hours. The paper is <a href=\"https:\/\/www.vldb.org\/pvldb\/vol12\/p2143-antonopoulos.pdf\">Constant Time Recovery in Azure SQL Database<\/a> (VLDB 2019), and it explains how recovery stopped depending on how much work your transaction did.<\/span><\/p>\n<h2><a name=\"_Toc237947375\"><\/a><span style=\"font-family: arial, helvetica, sans-serif;\">Why recovery was slow<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">Databases have used a recovery algorithm called ARIES since the early 90s (I did not know that before this paper, it stands for \u201cAlgorithms for Recovery and Isolation Exploiting Semantics\u201d). I\u2019m no expert, but I understand it runs three phases: analysis, redo, then undo. The undo phase is the problem. As the paper puts it:<\/span><\/p>\n<blockquote><p><span style=\"font-family: arial, helvetica, sans-serif;\">\u201cRecovering the database to a consistent state requires undoing all operations performed by uncommitted transactions which makes the cost of recovery proportional to the work performed by these transactions.\u201d<\/span><\/p><\/blockquote>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">Cost proportional to work done, yikes. A transaction that touched 50 million rows generates 50 million units of undo. On a departmental server in 1995, transactions were small (so I\u2019m told \u2013 I was definitely not doing anything with a departmental server in 1995!). In a cloud service running millions of databases, where hardware fails and maintenance restarts happen constantly, that proportionality becomes an availability problem.<\/span><\/p>\n<h2><a name=\"_Toc237947376\"><\/a><span style=\"font-family: arial, helvetica, sans-serif;\">The idea: stop undoing, start ignoring<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">SQL Server already had a version store for snapshot isolation, where updating a row keeps the old version around in tempdb so readers see a consistent picture. This has been a topic that is always fuzzy for me, but the paper helped me understand. The innovation of Constant Time Recovery, or CTR, takes that and makes it durable (i.e., not in tempdb).<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The<strong> Persistent Version Store (PVS)<\/strong> keeps row versions in the database itself, so they survive a restart. Versions live in two places: small changes are stored in-row as a byte diff next to the current row, larger ones go to an off-row table. By the way, it\u2019s pretty crazy that the database just does this for you. The more I learn about the engine, the more I am impressed.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Logical revert<\/strong> is the payoff. When a transaction aborts, the system doesn\u2019t walk the log undoing changes. It marks the transaction ID as aborted in an in-memory hash table called the <strong>Aborted Transaction Map<\/strong>. Any query that later encounters a row version written by that transaction skips it and reads the previous version instead. The abort is effectively instant. Cleanup happens lazily (I still smile that this is a technical term) in the background.<\/span><\/p>\n<blockquote><p><span style=\"font-family: arial, helvetica, sans-serif;\">\u201cThe database is fully available, releasing all locks, while row versions are lazily cleaned up in the background.\u201d<\/span><\/p><\/blockquote>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>The SLog<\/strong> handles the leftovers. A few operations can\u2019t be versioned, like certain metadata changes and lock acquisitions. Those go into a small secondary log stream that can be redone and undone in constant time, because there are very few of them.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The design choice I appreciate most is that they didn\u2019t rewrite the engine, they just got creative:<\/span><\/p>\n<blockquote><p><span style=\"font-family: arial, helvetica, sans-serif;\">\u201cCTR combines traditional ARIES with multi-version concurrency control to improve recovery time without having to fundamentally redesign the storage engine.\u201d<\/span><\/p><\/blockquote>\n<h2><a name=\"_Toc237947377\"><\/a><span style=\"font-family: arial, helvetica, sans-serif;\">The numbers<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">Recovery time, measured across production Azure SQL Database:<\/span><\/p>\n<table style=\"width: 26.3752%; border-style: inset;\" border=\"1\">\n<thead>\n<tr>\n<td style=\"width: 33.0472%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">Percentile<\/span><\/td>\n<td style=\"width: 38.1974%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">Traditional<\/span><\/td>\n<td style=\"width: 77.2532%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">CTR<\/span><\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"width: 33.0472%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">99.9th<\/span><\/td>\n<td style=\"width: 38.1974%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">68 s<\/span><\/td>\n<td style=\"width: 77.2532%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">60 s<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.0472%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">99.99th<\/span><\/td>\n<td style=\"width: 38.1974%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">over 10 min<\/span><\/td>\n<td style=\"width: 77.2532%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">1.3 min<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.0472%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">99.999th<\/span><\/td>\n<td style=\"width: 38.1974%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">over 1 hour<\/span><\/td>\n<td style=\"width: 77.2532%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">3 min<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The tail is where this matters. At the 99.999th percentile, over an hour became three minutes. Rollback also benefits from this, making it incredibly fast.<\/span><\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/10\/Picture1.webp\"><img decoding=\"async\" class=\"alignnone wp-image-7872 size-full\" src=\"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/10\/Picture1.webp\" alt=\"Picture1 image\" width=\"1172\" height=\"375\" srcset=\"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/10\/Picture1.webp 1172w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/10\/Picture1-300x96.webp 300w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/10\/Picture1-1024x328.webp 1024w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/10\/Picture1-768x246.webp 768w\" sizes=\"(max-width: 1172px) 100vw, 1172px\" \/><\/a><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><em>Source: Figure 9 from Constant Time Recovery in Azure SQL Database (VLDB 2019).<\/em><\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">There\u2019s a second benefit that surprised me the first time I read it. Because aborted work no longer has to stay in the log for undo purposes, the log can be truncated aggressively even while a huge transaction is running:<\/span><\/p>\n<table style=\"border-style: inset; width: 25.6039%;\" border=\"1\">\n<thead>\n<tr>\n<td style=\"width: 34.0996%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">Operation<\/span><\/td>\n<td style=\"width: 38.6973%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">Traditional log<\/span><\/td>\n<td style=\"width: 61.3027%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">CTR log<\/span><\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"width: 34.0996%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">10M inserts<\/span><\/td>\n<td style=\"width: 38.6973%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">5,101 MB<\/span><\/td>\n<td style=\"width: 61.3027%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">99 MB<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 34.0996%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">50M inserts<\/span><\/td>\n<td style=\"width: 38.6973%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">25,873 MB<\/span><\/td>\n<td style=\"width: 61.3027%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">185 MB<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 34.0996%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">50M deletes<\/span><\/td>\n<td style=\"width: 38.6973%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">38,255 MB<\/span><\/td>\n<td style=\"width: 61.3027%;\"><span style=\"font-family: arial, helvetica, sans-serif;\">147 MB<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">That 50 million row delete used 38 GB of log before. Now it uses 147 MB. If you\u2019ve ever had a maintenance job fill the transaction log and take an application down, you understand why that row matters. Crazy.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The cost is real but bounded, at least at the time of the paper. On TPC-C, an update-heavy workload, throughput dropped 13.8% with in-row versioning enabled (28% without it). On TPC-E, a more mixed workload, the hit was 2.4%. The team took a single-digit to low-double-digit throughput cost in exchange for removing hour-long outages from the tail.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">CTR was enabled worldwide for over three million databases in Azure SQL Database in May 2019.<\/span><\/p>\n<h2><a name=\"_Toc237947378\"><\/a><span style=\"font-family: arial, helvetica, sans-serif;\">Where you see this today<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">CTR ships as <a href=\"https:\/\/learn.microsoft.com\/sql\/relational-databases\/accelerated-database-recovery-concepts\">Accelerated Database Recovery<\/a>, always on in Azure SQL Database (you can\u2019t turn it off) and available in SQL Server since 2019.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">Practically, it means:<\/span><\/p>\n<ul>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Rolling back a big transaction is fast<\/strong>, regardless of size. Cancelling a runaway UPDATE no longer means waiting as long as the update ran.<\/span><\/li>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Recovery after failover is predictable<\/strong>, which is what makes an availability SLA meaningful.<\/span><\/li>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Long-running data loads don\u2019t blow up your log.<\/strong><\/span><\/li>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Optimized locking depends on it.<\/strong> The lock improvements shipping in Azure SQL today build directly on the persistent version store this paper introduced. I\u2019d like to do a blog on this area once I get to that paper as well.<\/span><\/li>\n<\/ul>\n<h2><a name=\"_Toc237947379\"><\/a><span style=\"font-family: arial, helvetica, sans-serif;\">Why this matters if you think relational databases are stuck in the past<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The interesting thing about CTR is that it\u2019s a new recovery algorithm, in production, in a system people assume stopped evolving decades ago. ARIES was the textbook answer for 25 years. This work changed the answer, and did it without breaking a single application.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The team also didn\u2019t chase elegance for its own sake. They kept ARIES where ARIES worked, added versioning where it helped, and put the awkward leftovers in a small side channel. That\u2019s engineering under constraints, and it\u2019s why it shipped to millions of databases instead of staying a paper.<\/span><\/p>\n<h2><span style=\"font-family: arial, helvetica, sans-serif;\">What\u2019s changed since the paper<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">The version store the paper introduced turned out to need its own tuning, and most of the follow-up work went there.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Cleanup got faster.<\/strong> The original design cleaned up old row versions with a single background thread, which could fall behind on busy systems and let the version store grow. SQL Server 2022 <a href=\"https:\/\/learn.microsoft.com\/sql\/relational-databases\/accelerated-database-recovery-concepts\">made that cleanup multi-threaded<\/a>, added cleanup for committed transactions rather than only aborted ones, and let user transactions help clean up when the background process is blocked. It also cut the memory used to track versions.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Rollback in <\/strong><strong>tempdb<\/strong><strong> got the same treatment.<\/strong> SQL Server 2025 <a href=\"https:\/\/learn.microsoft.com\/sql\/relational-databases\/accelerated-database-recovery-concepts\">extends ADR to tempdb<\/a>, so a large rollback involving temp tables no longer takes as long as the work that created them.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>It became a dependency.<\/strong> ADR is always on in Azure SQL Database and is now a prerequisite for optimized locking, which is I will write about eventually in the future. A 2019 recovery paper is the reason a 2024 locking redesign was possible.<\/span><\/p>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\">If you run SQL Server yourself, ADR is available but off by default, and the version store cleanup improvements are a good reason to be on a recent version before enabling it on a busy database.<\/span><\/p>\n<h2><span style=\"font-family: arial, helvetica, sans-serif;\">Credits<\/span><\/h2>\n<p><span style=\"font-family: arial, helvetica, sans-serif;\"><em>Constant Time Recovery in Azure SQL Database<\/em> was written by Panagiotis Antonopoulos, Peter Byrne, Wayne Chen, Cristian Diaconu, Raghavendra Thallam Kodandaramaih, Hanuma Kodavalla, Prashanth Purnananda, Adrian-Leonard Radu, Chaitanya Sreenivas Ravella, and Girish Mittur Venkataramanappa (Microsoft).<\/span><\/p>\n<h2><a name=\"_Toc237947380\"><\/a><span style=\"font-family: arial, helvetica, sans-serif;\">Next steps<\/span><\/h2>\n<ul>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Read the paper<\/strong>: <a href=\"https:\/\/www.vldb.org\/pvldb\/vol12\/p2143-antonopoulos.pdf\">Constant Time Recovery in Azure SQL Database<\/a> (VLDB 2019)<\/span><\/li>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Learn the feature<\/strong>: <a href=\"https:\/\/learn.microsoft.com\/sql\/relational-databases\/accelerated-database-recovery-concepts\">Accelerated Database Recovery<\/a><\/span><\/li>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Check your own workload<\/strong>: If you\u2019re on SQL Server, <a href=\"https:\/\/learn.microsoft.com\/sql\/relational-databases\/accelerated-database-recovery-management\">enable ADR<\/a> and compare rollback times on a large transaction<\/span><\/li>\n<li><span style=\"font-family: arial, helvetica, sans-serif;\"><strong>Next in this series<\/strong>: Always Encrypted, and querying data the database can\u2019t read<\/span><\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Under the Hood: The Research Behind Microsoft SQL, part 2 Here\u2019s a scenario that used to be genuinely frightening: you kick off a bulk load of a few hundred million rows in a single transaction. Somewhere in the middle, the server goes down. Not because of your load, just an unlucky reboot. When SQL Server [&hellip;]<\/p>\n","protected":false},"author":12403,"featured_media":7872,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,672],"tags":[529,482,753],"class_list":["post-7871","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-sql","category-sql-server-2025","tag-azure-sql","tag-hyperscale","tag-research"],"acf":[],"blog_post_summary":"<p>Under the Hood: The Research Behind Microsoft SQL, part 2 Here\u2019s a scenario that used to be genuinely frightening: you kick off a bulk load of a few hundred million rows in a single transaction. Somewhere in the middle, the server goes down. Not because of your load, just an unlucky reboot. When SQL Server [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7871","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/users\/12403"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/comments?post=7871"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7871\/revisions"}],"predecessor-version":[{"id":7917,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7871\/revisions\/7917"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media\/7872"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media?parent=7871"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/categories?post=7871"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/tags?post=7871"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}