Blocking is one of the concurrency problems people notice early in SQL Server. Under the default lock-based form of READ COMMITTED, a reader can end up waiting behind a writer even if the reader only wants the last committed value. READ_COMMITTED_SNAPSHOT, usually shortened to RCSI, changes that behavior at the database level. After it is turned on, statements that run at the READ COMMITTED isolation level read row versions instead of taking shared row and page locks, so they see a transactionally consistent view of committed data as of the start of each statement. Dirty reads still stay out, but resource usage moves in a different direction because updates and deletes now need older committed row images kept in a version store.
What Read Committed Snapshot Changes
Read committed can follow two different read paths in SQL Server, and the switch between them is the READ_COMMITTED_SNAPSHOT database option. With that option off, READ COMMITTED protects reads with shared locks. With that option on, the same isolation level reads from row versions instead. Dirty reads stay out in both cases, but the way readers and writers interact changes in a very visible way. On boxed SQL Server, READ_COMMITTED_SNAPSHOT is off by default. In Azure SQL Database and SQL database in Microsoft Fabric, it is on by default.
Default Read Committed
Most SQL Server databases begin with lock-based READ COMMITTED. Under that behavior, a SELECT takes shared locks while it reads data so it does not read rows that are still being changed by an uncommitted transaction. SQL Server releases those locks as the read moves forward. Row locks are released before the next row is processed, page locks are released when the next page is read, and table locks are released when the statement finishes. That gives the statement committed data, but it also ties read activity to the current lock state of the target rows.
Blocking comes from that lock interaction. If one session has changed a row and has not committed yet, a second session running a plain SELECT at READ COMMITTED does not read the uncommitted value and does not skip over it. It waits for the writer to finish. That behavior is part of how SQL Server keeps dirty reads out of ordinary queries, and it is one reason read-heavy workloads can slow down when long write transactions stay open. Microsoft’s blocking guidance calls out disabled RCSI as a direct cause of higher blocking for SELECT statements running at READ COMMITTED.
Take a basic read like this:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
SELECT SalesOrderID, OrderDate, TotalDue
FROM Sales.SalesOrderHeader
WHERE CustomerID = 11000;
GOWith READ_COMMITTED_SNAPSHOT off, SQL Server treats that statement as a locking read. Nothing in the query text has to mention locks for that to happen. The shared locks come from the isolation level rules themselves. That is why a statement that looks completely ordinary can still wait behind an active update touching the same rows.
What RCSI Changes
Turn on READ_COMMITTED_SNAPSHOT, and the isolation level name stays READ COMMITTED while the data access method changes. SQL Server now uses row versioning to give each statement a transactionally consistent view of committed data as it existed at the start of that statement. Shared row and page locks are no longer used to protect the read from concurrent updates, so a reader can keep going without waiting behind an open writer in the usual reader versus writer case.
This query is a quick way to confirm which path a database is on:
SELECT name,
is_read_committed_snapshot_on,
snapshot_isolation_state_desc
FROM sys.databases
WHERE name = DB_NAME();
GOThat check separates two things people regularly blend together. is_read_committed_snapshot_on = 1 means ordinary READ COMMITTED statements use row versioning. snapshot_isolation_state_desc = ON means the separate SNAPSHOT isolation level is allowed for sessions that explicitly request it. Those are related features, but they are not the same feature.
Statement scope is one of the biggest behavior changes. RCSI gives a snapshot as of the start of the current statement, not as of the start of the whole transaction. That means two SELECT statements inside one explicit transaction can still see different committed values if some other session commits a change between the first statement and the second one. Full SNAPSHOT isolation behaves differently because it keeps a transaction-level view instead of refreshing the view at each statement boundary.
Locking does not disappear from the database after RCSI is enabled. Data modifications still use write-side locking, and UPDATE, INSERT, and DELETE operations can still block other writes when they need the same protected resource. That point is easy to miss when people hear that RCSI reduces blocking. Reader versus writer waits are reduced, but writer versus writer waits still exist because data changes still need their normal protection.
There are also times when you may want locking reads back for a specific statement in a database that has RCSI enabled. SQL Server supports that through the READCOMMITTEDLOCK table hint:
SELECT ProductID, Name, ListPrice
FROM Production.Product WITH (READCOMMITTEDLOCK)
WHERE ProductID = 870;
GOThat hint tells SQL Server to honor READ COMMITTED with shared locking for that statement instead of row versioning. It can be useful when a statement needs the older locking behavior on purpose, but it also gives up the usual read-side concurrency benefit that RCSI brings.
Schema locking is still part of the picture too. Queries running with row versioning do not take shared data locks for the rows they read, yet SQL Server still acquires schema stability locks during compilation and execution. Those locks do not conflict with normal data modification locks, but they can still interact with schema modification activity such as DDL. So RCSI changes the read path for data, not the fact that the engine still has to protect table metadata while a query runs.
Row Versioning in Practice
Versioned reads change more than wait behavior. They add a second copy path for changed rows, they place storage pressure in a version store, and they make transaction length far more visible than it used to be under pure lock-based reads. That is why RCSI usually gets explained best in terms of what SQL Server stores, where it stores it, and how long those row copies stay alive.
How SQL Server Builds Versions
Row versioning in SQL Server follows a copy-before-change model. When READ_COMMITTED_SNAPSHOT or ALLOW_SNAPSHOT_ISOLATION is on, SQL Server keeps logical copies for data modifications in that database. On every row change, the engine stores the previously committed image of the row in the version store and stamps that version with a transaction sequence number, usually shortened to XSN. The newest row value stays in the base table, and older committed images are linked behind it in a version chain.
That XSN detail helps explain why statement-level reads under RCSI behave the way they do. A transaction can start with BEGIN TRANSACTION, but the XSN is assigned on the first qualifying read or write after that point. For row-versioned READ COMMITTED, SQL Server does not hold one snapshot for the full transaction. It reads the latest issued XSN at the start of each statement and uses that marker to decide which version belongs to that statement’s view. Full SNAPSHOT isolation uses its own transaction view instead.
Large values follow the same general rule with one narrower detail. If a large object column is changed, SQL Server copies only the fragment that changed into the version store rather than copying the full large value every time. That keeps version generation from growing more than needed for LOB updates.
Version cleanup is tied closely to transaction lifetime. SQL Server holds row versions long enough for active versioned transactions to read them, tracks the earliest useful XSN, and periodically removes versions older than that point. Long-running transactions delay that cleanup. The same thing can happen with transactions tied to triggers, MARS sessions, online index activity, or statements that generated versions themselves. Under RCSI, the read view is statement-based, but row versions created inside the transaction can still be kept until the whole transaction ends.
Where Versions Live Today
Older RCSI write-ups usually point straight to tempdb, and that is still true on databases without ADR. In that case, the common version store for row-versioned reads and writes lives in tempdb, and SQL Server tracks its size there. That part of the engine has not gone away. What changed in newer releases is ADR. When Accelerated Database Recovery is enabled, row versions can be stored in the persistent version store, or PVS, inside the user database instead of the older shared store in tempdb.
That storage split changes where pressure appears. If the version store is in tempdb, sys.dm_tran_version_store_space_usage gives a per-database view of reserved version-store pages and space in kilobytes. The DMV is aggregated, so it is a light way to see which database is taking the space without scanning individual version rows:
SELECT
DB_NAME(database_id) AS DatabaseName,
reserved_page_count,
reserved_space_kb
FROM sys.dm_tran_version_store_space_usage
ORDER BY reserved_space_kb DESC;
GOThat query is handy when a server suddenly starts burning through tempdb and you need to see which database is feeding the version store.
ADR changes the monitoring target. In that case, sys.dm_tran_persistent_version_store_stats reports PVS metrics such as the filegroup that hosts PVS and the size of off-row versions. This DMV applies to ADR-backed persistent version storage in SQL Server 2019 and newer, Azure SQL Database, Azure SQL Managed Instance, and SQL database in Microsoft Fabric.
SELECT
DB_NAME(database_id) AS DatabaseName,
pvs_filegroup_id,
persistent_version_store_size_kb,
online_index_version_store_size_kb,
current_aborted_transaction_count,
oldest_active_transaction_id
FROM sys.dm_tran_persistent_version_store_stats
WHERE database_id = DB_ID();
GOPVS size by itself does not tell the full story, because a single long transaction can keep old versions alive far longer than expected. sys.dm_tran_active_snapshot_database_transactions fills that gap by listing active transactions that generate row versions or can read them. That DMV also includes activity related to triggers, MARS, and online indexing, which is useful when version cleanup looks stuck and no ordinary snapshot query seems long enough to explain it.
Space pressure behaves differently depending on the storage location. If the version store is in tempdb and space runs out, SQL Server can force version-store shrink activity, mark long transactions as victims, keep write statements running without generating fresh versions, and fail reads that need versions that are no longer available. With ADR-backed PVS, reads continue to succeed, while writes that need to generate versions such as UPDATE and DELETE can fail when the store is full.
Turning RCSI On
RCSI is a database option, not a session switch. You turn it on with ALTER DATABASE, and after that ordinary READ COMMITTED statements in that database move to row-versioned reads. That does not turn on explicit SNAPSHOT isolation by itself. ALLOW_SNAPSHOT_ISOLATION is still a separate database option for sessions that want transaction-level snapshot semantics.
Changing the option has one operational rule that catches people during release windows. While ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ON is running, only the session issuing that command can be connected to the database. SQL Server does not require single-user mode for the change, but it does require every other connection to be gone until the command finishes.
USE master;
GO
ALTER DATABASE SalesLab
SET READ_COMMITTED_SNAPSHOT ON;
GO
SELECT
name,
is_read_committed_snapshot_on,
snapshot_isolation_state_desc
FROM sys.databases
WHERE name = N'SalesLab';
GOThat batch both flips the setting and checks the result. is_read_committed_snapshot_on = 1 means READ COMMITTED statements in that database now read versioned rows. If you later decide the database also needs explicit SNAPSHOT transactions, that is a separate ALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON change with its own state management. Microsoft notes that ALLOW_SNAPSHOT_ISOLATION can pass through PENDING_ON or PENDING_OFF, while RCSI activates its support mechanism right away after the option change completes.
Blocking Demo Across Two Sessions
Seeing the difference in two windows makes the storage story much easier to connect to day-to-day query behavior. Start with a tiny table in a test database that already has RCSI turned on or off, depending on which pass you want to watch first:
USE SalesLab;
GO
DROP TABLE IF EXISTS dbo.StockLevel;
GO
CREATE TABLE dbo.StockLevel
(
ItemID int NOT NULL PRIMARY KEY,
QtyOnHand int NOT NULL
);
GO
INSERT INTO dbo.StockLevel (ItemID, QtyOnHand)
VALUES (1001, 25);
GOThat row gives one target that both sessions can touch without extra joins or filters getting in the way.
Open Session 1 and leave a transaction active long enough to test a concurrent read:
BEGIN TRANSACTION;
UPDATE dbo.StockLevel
SET QtyOnHand = 18
WHERE ItemID = 1001;
WAITFOR DELAY '00:00:12';
COMMIT TRANSACTION;
GOWhile that transaction is waiting, run this in Session 2:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
SELECT ItemID, QtyOnHand
FROM dbo.StockLevel
WHERE ItemID = 1001;
GOWith lock-based READ COMMITTED, Session 2 waits for Session 1 to finish because the reader is trying to read a row that is still locked by an uncommitted writer. With RCSI on, the SELECT can return the last committed row version for that statement instead of waiting on the row being changed. Dirty data still stays out. What changes is the source of the read, not the isolation level name in the session.
Run the same SELECT again after Session 1 commits. The first read during the open transaction returns the older committed quantity. The later read returns the new committed quantity. That is statement-level versioning in plain form. One statement reads as of its own start time, and the next statement can see a later commit.
Reader blocking is what changes most visibly, but write blocking still remains. Keep Session 1 open again with the same update pattern, then try this in Session 2:
UPDATE dbo.StockLevel
SET QtyOnHand = QtyOnHand - 2
WHERE ItemID = 1001;
GOThat second write still waits on the first write. RCSI does not remove exclusive locks from data modification, so writer versus writer blocking stays part of normal concurrency. That same idea applies more broadly in Azure SQL too. RCSI cuts down blocking between readers and writers, but write operations can still block other write operations.
Some code paths still need locking reads after RCSI is enabled, but that choice belongs to individual statements rather than the whole database read path. The broader point from this demo is that turning on RCSI changes where the wait usually appears. Short reads stop waiting behind uncommitted writes as frequently, and the storage cost moves into row-version maintenance and cleanup.
Conclusion
Read committed snapshot changes READ COMMITTED by moving reads away from shared-lock waiting and toward committed row versions tied to statement start time. That change cuts reader-writer blocking, but it also means SQL Server has to keep older row images, track transaction lifetime more closely, and manage version-store space in tempdb or in the database through PVS when ADR is active. Simply put, the read path gets less dependent on current data locks, while the storage side of concurrency becomes a bigger part of how the database behaves.


