Topic page · SQL Server TempDB

Understanding TempDB: Spot Problems and Relieve the Load

TempDB is the shared workspace of the entire instance. Every database uses it: for temporary tables, for sorts that do not fit into memory, for row versions, and for much that does not appear as TempDB in the execution plan at all. If TempDB fills up, it hits all applications on the server at the same time, including those that did not cause the problem.

Updated Nov 2, 2026 · approx. 6 min read

In depth 3SQL queries 3Tested SQL Server 2017–2025
TempDBUser objects#temp, @tableInternal objectsSort, Hash, SpoolVersion storerow versionsLog(log file)Data file

The good news: TempDB is not a black hole. SQL Server knows exactly which kind of work occupies how much space and which session or transaction is holding on to it. This page explains the consumers, shows the current state with a read-only SQL query, and leads to the deep dives. You do not need any PSG tool for this.

Who is using TempDB

SQL Server distinguishes four areas. The columns in the query below follow exactly this breakdown.

AreaTypical consumersIn the queryDeep dive
User objects#temp tables, table variables, global ##temp tables, index builds with SORT_IN_TEMPDBuser_objects_mb–
Internal objectsSorts and hash joins that spill, spools, cursors, large intermediate resultsinternal_objects_mbSpills and spools
Version storeRead Committed Snapshot, snapshot isolation, triggers, online index operations, if Accelerated Database Recovery (ADR) is off for the databaseversion_store_mbVersion store and transaction log
TempDB logChanges to temporary objects; open transactions prevent it from being freed againlog_mb, log_wait_reasonVersion store and transaction log

The first three areas are in the data files of TempDB, the fourth in the log. A full TempDB log (error 9002) and full data files (error 1105) therefore have different causes.

The deep dives

Which observation matches yours?

Your observationFirst lookDeep dive
You are setting up TempDB or reviewing files and sizeCheck the number, size, and placement of the filesSetting up TempDB
TempDB is growing, and you don't know whySnapshot, result 1: which area is growingDepends on the area, see above
A query writes a lot to TempDB, and the plan shows a warning or a spoolGet the actual execution planSpills and spools
The version store grows over hoursSnapshot, result 3: oldest snapshot transactionVersion store and transaction log
The TempDB log is full (error 9002, ACTIVE_TRANSACTION)Snapshot, result 4: open transactionsVersion store and transaction log
Everything is fine again after a restartSave the evidence before the next restartWhy does restarting SQL Server help?

Snapshot: What is using TempDB right now?

The query returns a snapshot in four results: the usage by area, the sessions with the largest space requirements, the version store with the oldest snapshot transactions, and the state of the TempDB log. Run it while the problem exists, and if needed several times at intervals of a few minutes.

Query: TempDB snapshot

Query 1: TempDB snapshot · read-only · tested on SQL Server 2017–2025
/* PSG Stack · SQL Server TempDB · Query 1
   TempDB snapshot: Who is using space right now, and what is holding on to it?

   - Read-only. Changes nothing, ends no session.
   - Every part runs with MAXDOP 1.
   - Returns four results:
       1 Usage of the TempDB data files by type
       2 Sessions with the largest TempDB usage
       3 Version store: size per database and oldest snapshot transactions
       4 TempDB log: fill level, wait reason and open transactions
   - Permissions: VIEW SERVER STATE
     (from SQL Server 2022, VIEW SERVER PERFORMANCE STATE is enough).
     Without this permission, every part fails with error 300 (VIEW DATABASE STATE alone is not enough).
   - SQL Server 2017 and later. Tested in the PSG lab on 2017, 2019, 2022, and 2025.
   - Runs in any database: TempDB values always come from TempDB.
   - Snapshot: values change continuously. Run it several times if needed.
*/

/* 1 · Usage of the TempDB data files by type (MB) */
SELECT
    SUM(fsu.user_object_reserved_page_count)     * 8 / 1024 AS user_objects_mb,
    SUM(fsu.internal_object_reserved_page_count) * 8 / 1024 AS internal_objects_mb,
    SUM(fsu.version_store_reserved_page_count)   * 8 / 1024 AS version_store_mb,
    SUM(fsu.mixed_extent_page_count)             * 8 / 1024 AS mixed_extents_mb,
    SUM(fsu.unallocated_extent_page_count)       * 8 / 1024 AS free_mb,
    COUNT(*)                                                AS data_files
FROM tempdb.sys.dm_db_file_space_usage AS fsu
OPTION (MAXDOP 1);

/* 2 · Sessions with the largest TempDB usage
   Completed tasks (session_space_usage) plus running tasks (task_space_usage).
   User objects: #temp tables, table variables, index builds with SORT_IN_TEMPDB.
   Internal objects: sort, hash, spools, cursors, LOB intermediate results.
   The counters apply per session: if another session drops an object (e.g., a global ## table),
   the space stays charged to the creating session until it ends. The sum can therefore
   exceed result 1. */
WITH space_used AS (
    SELECT ssu.session_id,
           ssu.user_objects_alloc_page_count     - ssu.user_objects_dealloc_page_count     AS user_pages,
           ssu.internal_objects_alloc_page_count - ssu.internal_objects_dealloc_page_count AS internal_pages
    FROM sys.dm_db_session_space_usage AS ssu
    WHERE ssu.database_id = 2
    UNION ALL
    SELECT tsu.session_id,
           tsu.user_objects_alloc_page_count     - tsu.user_objects_dealloc_page_count,
           tsu.internal_objects_alloc_page_count - tsu.internal_objects_dealloc_page_count
    FROM sys.dm_db_task_space_usage AS tsu
    WHERE tsu.database_id = 2
), per_session AS (
    SELECT session_id,
           SUM(user_pages)     AS user_pages,
           SUM(internal_pages) AS internal_pages
    FROM space_used
    GROUP BY session_id
)
SELECT TOP (20)
    js.session_id,
    es.login_name,
    es.host_name,
    es.program_name,
    es.status,
    CAST(CASE WHEN js.user_pages     > 0 THEN js.user_pages     ELSE 0 END * 8 / 1024.0 AS decimal(12,1)) AS user_objects_mb,
    CAST(CASE WHEN js.internal_pages > 0 THEN js.internal_pages ELSE 0 END * 8 / 1024.0 AS decimal(12,1)) AS internal_objects_mb,
    r.command,
    r.wait_type,
    r.granted_query_memory * 8 / 1024                       AS memory_grant_mb,
    DB_NAME(r.database_id)                                  AS database_name,
    LEFT(st.text, 4000)                                     AS sql_text
FROM per_session AS js
JOIN sys.dm_exec_sessions AS es ON es.session_id = js.session_id
LEFT JOIN sys.dm_exec_requests AS r ON r.session_id = js.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS st
WHERE es.is_user_process = 1
  AND (js.user_pages > 0 OR js.internal_pages > 0)
ORDER BY (CASE WHEN js.user_pages     > 0 THEN js.user_pages     ELSE 0 END
        + CASE WHEN js.internal_pages > 0 THEN js.internal_pages ELSE 0 END) DESC
OPTION (MAXDOP 1);

/* 3a · Version store in TempDB per database (MB) */
SELECT
    DB_NAME(vs.database_id)                       AS database_name,
    vs.reserved_page_count * 8 / 1024             AS version_store_mb
FROM sys.dm_tran_version_store_space_usage AS vs
WHERE vs.reserved_page_count > 0
ORDER BY vs.reserved_page_count DESC
OPTION (MAXDOP 1);

/* 3b · Oldest snapshot transactions: they hold on to the version store
   snapshot_isolation = 0: transaction without SNAPSHOT isolation that creates row versions
   (e.g., writers under READ_COMMITTED_SNAPSHOT). */
SELECT TOP (10)
    ast.session_id,
    ast.elapsed_time_seconds                      AS open_s,
    ast.is_snapshot                               AS snapshot_isolation,
    es.login_name,
    es.host_name,
    es.program_name,
    es.status,
    es.last_request_end_time                      AS last_request_end
FROM sys.dm_tran_active_snapshot_database_transactions AS ast
LEFT JOIN sys.dm_exec_sessions AS es ON es.session_id = ast.session_id
ORDER BY ast.elapsed_time_seconds DESC
OPTION (MAXDOP 1);

/* 4a · TempDB log: size, fill level and wait reason */
SELECT
    CAST(ls.total_log_size_in_bytes / 1048576.0 AS decimal(12,1)) AS log_mb,
    CAST(ls.used_log_space_in_percent AS decimal(5,1))             AS used_percent,
    d.log_reuse_wait_desc                                          AS log_wait_reason
FROM tempdb.sys.dm_db_log_space_usage AS ls
CROSS JOIN sys.databases AS d
WHERE d.database_id = 2
OPTION (MAXDOP 1);

/* 4b · Open transactions with TempDB log usage, largest first */
SELECT TOP (10)
    st.session_id,
    dt.database_transaction_begin_time                                    AS began_at,
    DATEDIFF(SECOND, dt.database_transaction_begin_time, SYSDATETIME())   AS open_s,
    CAST(dt.database_transaction_log_bytes_used     / 1048576.0 AS decimal(12,1)) AS log_used_mb,
    CAST(dt.database_transaction_log_bytes_reserved / 1048576.0 AS decimal(12,1)) AS log_reserved_mb,
    es.login_name,
    es.program_name,
    es.status
FROM sys.dm_tran_database_transactions AS dt
JOIN sys.dm_tran_session_transactions AS st ON st.transaction_id = dt.transaction_id
LEFT JOIN sys.dm_exec_sessions AS es ON es.session_id = st.session_id
WHERE dt.database_id = 2
ORDER BY dt.database_transaction_log_bytes_used DESC
OPTION (MAXDOP 1);

How to read the result:

  • Result 1 shows which area is growing. If the space is in internal_objects_mb, look in result 2 for running queries with a large memory grant or large sorts. If it is in version_store_mb, result 2 usually does not help: versions are not attributed to any session.
  • Result 2 names the sessions with their own space requirements. A sleeping session with many user objects often holds a large #temp table that only disappears with the connection. The counters apply per session: if another session drops an object, such as a global ## table, the space stays charged to the creating session until it ends. The sum can therefore exceed result 1.
  • Result 3 shows for which database versions are being created and which snapshot transaction has held them the longest. open_s in the range of hours with status “sleeping” is a typical holding transaction.
  • Result 4 shows why the TempDB log is not being freed. With ACTIVE_TRANSACTION, the cause is in the list of open transactions. If the log is still only slightly filled, NOTHING often still appears despite an open transaction; the list in 4b is then what counts.

Sample output from a reproduced lab run (SQL Server 2025, synthetic data, names chosen freely, columns shortened). Session 56 filled a #temp table and left its transaction open, session 54 holds an open cursor, session 53 reads in SNAPSHOT isolation without finishing:

Sample output
1 · Usage by type (MB)
user_objects_mb  internal_objects_mb  version_store_mb  free_mb
40               48                   53                127

2 · Sessions with the largest usage
session_id  status    user_objects_mb  internal_objects_mb
54          sleeping  0.0              42.4
56          sleeping  39.2             0.0
60          running   0.0              6.0

3b · Oldest snapshot transactions
session_id  open_s  snapshot_isolation  status
53          25      1                   sleeping

4a · TempDB log
log_mb  used_percent  log_wait_reason
136.0   99.5          ACTIVE_TRANSACTION

4b · Open transactions with TempDB log usage
session_id  open_s  log_used_mb  status
56          20      78.0         sleeping

Reading it: the version store (53 MB) is not attributed to any session in result 2; it is held by session 53 in result 3b. The log is almost full because session 56 does not end its transaction. Both sessions are asleep: they are not working, they only hold on.

Three common assumptions that are not true

“Temporary tables are not logged.” Changes in TempDB are logged, just to a lesser extent than in user databases: they have to be rolled back, but not recovered after a crash. A long transaction that writes to #temp tables can therefore fill the TempDB log just like any other.

“The session with the most TempDB space is the culprit.” For user and internal objects, that is usually true. For the version store, it is not: the versions are created by the writing sessions, but they are held by the oldest transaction that could still read them. That transaction often has not occupied a single page itself.

“Shrinking solves the problem.” As long as the cause persists, TempDB grows again. Shrinking during operation can also fail because of pages in use. Microsoft describes the options and limits in a separate article (see Sources). First find and end whatever occupies the space.

What provides lasting relief

  • Set up TempDB appropriately. Several data files of equal size and enough space from the start avoid waits during space allocation and growth at the wrong moment. How many files, where, and how large is shown in Setting up TempDB. This does not replace knowing the consumers.
  • Avoid work instead of making space. A suitable index saves sorts and temporary indexes, and a better estimate prevents spills. What this looks like in the execution plan is shown in Spills and spools.
  • Keep transactions short. Every open transaction holds on to versions and log. More in Version store and transaction log.
  • Use temporary tables deliberately. A #temp table with a few filtered rows can simplify a query considerably. A #temp table that copies half a table only shifts work to TempDB.
  • Widen your view. If TempDB is only a symptom and the server as a whole is slow, the SQL Server performance guide leads you through the matching diagnostic paths.

Sources