Builds a database-scoped event session, starts it, reads its target back as a rowset, and drops it. What the captured data means belongs to the diagnostic skills that consume it.
Checked 2026-09-03 against Microsoft Learn's Azure SQL Database Extended Events pages and its
Database Engine error tables, and on 2026-08-29 against a local Azure SQL Database container
reporting EngineEdition 5. Where the two disagree this file follows Learn, because the
container is a different engine build and the subject here is the cloud service. Every
disagreement is named at the point it matters.
Four things the service changes, and the error number for each
| Scope | Sessions are database-scoped only. ON SERVER fails with Msg 25737, and so does ON DATABASE against a system database |
| Names | Catalog views and DMVs take a database_ prefix, never server_: sys.database_event_sessions, sys.dm_xe_database_sessions |
| Durable target | event_file writes to an Azure Storage blob and nowhere else. A local path fails at CREATE with Msg 40538 |
| No baseline | There is no built-in system_health session in Azure SQL Database. Nothing is capturing anything until you build a session |
That last row changes behaviour. On SQL Server an agent leans on system_health for deadlocks and
skips building anything; here Learn states there is no such session, so there is no fallback and no
master connection that reveals one. There is an internal dl session, and Learn's instruction
is not to read it: sys.fn_xe_file_target_read_file over a large dl file can raise an
out-of-memory error in master and disrupt login processing.
Steps
1. Write the session, with the ACTION() clauses attribution needs
An event carries only its own native fields. Who ran the statement, from what application, and the statement text are actions, and an event added without them captures none of the three.
CREATE EVENT SESSION capture_statements ON DATABASE
ADD EVENT sqlserver.sql_statement_completed (
ACTION (sqlserver.sql_text, sqlserver.username, sqlserver.client_app_name)
WHERE duration > 500000
)
ADD TARGET package0.ring_buffer (SET max_memory = 4096)
WITH (MAX_MEMORY = 4 MB, MAX_DISPATCH_LATENCY = 5 SECONDS, STARTUP_STATE = OFF);
Two details in that statement earn their place:
duration > 500000is half a second, not eight minutes.sql_statement_completedandrpc_completedreportdurationin microseconds;wait_infoandwait_completedreport it in milliseconds. A threshold copied between them is wrong by a factor of 1000 and the session gives no sign of it. Confirm the unit for the event you are actually using before you write a predicate, with the query in step 5.STARTUP_STATEdecides whether the session comes back after a failover: Learn's guidance isONfor a continuous session,OFFfor ad hoc troubleshooting. A ring buffer is cleared whenever the session stops, soONrestarts the session and still loses the events.
2. Create and start it on a connection to the user database
Creating a session does not start it. Run both, on one connection, to the user database and never
to master:
cat > capture-session.sql <<'SQL'
ALTER EVENT SESSION capture_statements ON DATABASE STATE = START;
SQL
sqlcmd -S <server-name>.database.windows.net -d <database-name> -G -N -b -i capture-session.sql
-G selects Microsoft Entra authentication (-U <login> -P instead, for a SQL authentication
login), -N requests an encrypted connection, and -b makes sqlcmd exit non-zero on error, so a
failed START fails the script instead of scrolling past.
Creating the session needs CREATE ANY DATABASE EVENT SESSION in the database and starting it
needs ALTER ANY DATABASE EVENT SESSION. These are the database-scoped permissions, not the
ALTER ANY EVENT SESSION server permission an example written for SQL Server will ask for. Both
are included in CONTROL on the database, held by dbo, by db_owner and by the server
administrator.
3. Read the ring buffer back as a rowset
target_data arrives as XML. Shred it rather than eyeballing it:
sqlcmd -S <server-name>.database.windows.net -d <database-name> -G -N -b -Q "
WITH RingBuffer AS (
SELECT CAST(xst.target_data AS xml) AS TargetData
FROM sys.dm_xe_database_session_targets AS xst
JOIN sys.dm_xe_database_sessions AS xs ON xst.event_session_address = xs.address
WHERE xs.name = N'capture_statements' AND xst.target_name = N'ring_buffer'
),
EventNode AS (
SELECT CAST(n.NodeData.query('.') AS xml) AS EventInfo
FROM RingBuffer AS rb CROSS APPLY rb.TargetData.nodes('/RingBufferTarget/event') AS n(NodeData)
)
SELECT EventInfo.value('(event/@timestamp)[1]', 'datetimeoffset') AS event_time,
EventInfo.value('(event/data[@name=\"duration\"]/value)[1]', 'bigint') AS duration,
EventInfo.value('(event/action[@name=\"username\"]/value)[1]', 'sysname') AS username,
EventInfo.value('(event/action[@name=\"client_app_name\"]/value)[1]', 'nvarchar(128)') AS app_name,
EventInfo.value('(event/action[@name=\"sql_text\"]/value)[1]', 'nvarchar(max)') AS sql_text
FROM EventNode
ORDER BY event_time DESC;"
The three action columns come back NULL for a session built without the ACTION() clause in
step 1. Event counts are identical either way, so this is the only place the difference shows.
4. Stop and drop when the capture is done
ALTER EVENT SESSION capture_statements ON DATABASE STATE = STOP;
DROP EVENT SESSION capture_statements ON DATABASE;
Stopping empties a ring buffer, so read it first. A stopped session still appears in
sys.database_event_sessions and disappears from sys.dm_xe_database_sessions; only DROP removes
the definition. Sessions are database-scoped, so dropping the database takes them with it.
5. The three metadata queries worth keeping
-- the duration unit for one event, read rather than assumed
SELECT o.name AS event_name, c.name AS column_name, c.description
FROM sys.dm_xe_objects AS o
JOIN sys.dm_xe_object_columns AS c ON c.object_name = o.name AND c.object_package_guid = o.package_guid
WHERE o.name IN (N'sql_statement_completed', N'wait_info') AND c.name = N'duration';
-- whether an event, action or target exists on this platform at all
SELECT o.object_type, o.name, o.description
FROM sys.dm_xe_objects AS o
WHERE o.object_type IN ('action', 'event', 'target') AND o.name LIKE '%deadlock%';
-- memory this database is spending on started sessions, against the 128 MB cap
SELECT name AS session_name, total_buffer_size + total_target_memory AS total_session_memory
FROM sys.dm_xe_database_sessions;
Session memory is capped at 128 MB per single database and 512 MB across an elastic pool, and
started sessions at 100 per database or pool. Exceeding the memory cap raises Msg 25746 or
Msg 25747, which name sys.dm_xe_database_sessions and tell you to stop or shrink a session.
Check it worked
Run this immediately after step 2. It is four assertions with stated expected values, and each one distinguishes a specific silent failure from success.
SET NOCOUNT ON;
DECLARE @name sysname = N'capture_statements', @fail int = 0, @n int;
-- 1. the definition exists. Zero here means CREATE never ran, or ran in another database
SELECT @n = COUNT(*) FROM sys.database_event_sessions WHERE name = @name;
IF @n <> 1 BEGIN PRINT 'FAIL 1: no session definition. CREATE did not run in this database'; SET @fail = 1; END
-- 2. it is STARTED. The catalog view above says nothing about this, and this is the usual answer
-- to "the session exists and the ring buffer is empty"
SELECT @n = COUNT(*) FROM sys.dm_xe_database_sessions WHERE name = @name;
IF @n <> 1 BEGIN PRINT 'FAIL 2: defined but never started. Run ALTER EVENT SESSION capture_statements ON DATABASE STATE = START'; SET @fail = 1; END
-- 3. the actions are attached. Expect 3: sql_text, username, client_app_name
SELECT @n = COUNT(*)
FROM sys.database_event_session_actions AS a
JOIN sys.database_event_sessions AS s ON s.event_session_id = a.event_session_id
WHERE s.name = @name;
IF @n <> 3 BEGIN PRINT CONCAT('FAIL 3: ', @n, ' actions attached, expected 3. Captured rows will have no caller'); SET @fail = 1; END
-- 4. the target is processing events. Zero AFTER a workload that should match the predicate means
-- the predicate, the unit, or the event choice is wrong, not the session
SELECT @n = ISNULL(SUM(execution_count), 0) FROM sys.dm_xe_database_session_targets AS t
JOIN sys.dm_xe_database_sessions AS s ON s.address = t.event_session_address WHERE s.name = @name;
IF @n = 0 BEGIN PRINT 'WARN 4: target has processed nothing yet. Expected after a matching workload'; END
IF @fail = 1 THROW 50001, 'event session is not capturing', 1;
PRINT 'event session: DEFINED, STARTED, ACTIONS ATTACHED';
Assertions 1 and 2 are the pair that gets read backwards. The catalog view lists every session definition whether or not it runs; the DMV lists only started sessions. Read one for the other and you get the opposite of the truth about whether a capture is live.
Sessions that create, start and capture nothing
Three cases, and none of them raise an error at any point.
- No
ACTION()clause. Events fire and are counted, and every attribution column isNULL. Assertion 3 above is what catches it. blocked_process_report. Its threshold is set withsp_configure 'blocked process threshold', an option Learn documents for SQL Server only, and which refusal you get depends on which database you are connected to, so quote the one you are on. Measured 2026-09-05 on a real logical server: there is nosp_configureat all, qualified or not, and the answer isMsg 2812, Could not find stored procedure 'sp_configure'. The container answers the same way from a user database, re-measured 2026-09-06 on 18.0.226_4_147, which is a correction: this skill said the container differed. Connected tomasterthe procedure is there and the answer isMsg 15123, The configuration option 'blocked process threshold' does not exist, because that option is not one this engine carries.Msg 40510, Statement 'CONFIG' is not supported in this version of SQL Servercomes fromRECONFIGUREand not fromsp_configure, and an option the engine does carry stages happily beforeRECONFIGURErefuses to apply it. Either waysys.database_scoped_configurationshas no equivalent key. Measured on the container: this event created, started and captured zero across a 28 second real lock wait. The supported route to blocked process reports here is not an event session at all: enable theBlocksdiagnostic settings category and readblocked_process_filtered_sout of the streamed resource logs.- An event or action the platform does not carry. These are rejected at
CREATErather than silently:Msg 25742for a target,Msg 25743for an event,Msg 25744for an action. Check with the second query in step 5 first.
On deadlocks, Learn and the container disagree, and this file follows Learn. Learn's documented
way to capture deadlock graphs on Azure SQL Database is a database-scoped session on
sqlserver.database_xml_deadlock_report. On the container that event created, started and captured
zero events across two confirmed deadlocks (Msg 1205 each time), while sqlserver.lock_deadlock
captured one. That gap has not been reproduced on the cloud service and is not evidence about it,
so build the session Learn documents and verify it with assertion 4 above against a deadlock you
cause deliberately. Keep lock_deadlock as the fallback only if that verification comes back empty.
When a session runs clean and its target stays empty, open
references/sessions-that-capture-nothing.md for the
captured XML of each case above, the container measurements behind them, and the
sys.dm_xe_object_columns duration text for a specific event.
The event_file target, in one paragraph
A local filesystem path is rejected at CREATE, not left unwritten:
Msg 40538, A valid URL beginning with 'https://' is required as value for any filepath specified.
That same message is also what a malformed blob URL gets, and it names the wrong thing: measured
on a logical server 2026-09-08, a container segment of fewer than three characters is refused with
Msg 40538 while the identical URL with a longer container name is accepted, so a reader retypes a
scheme that was already https://. Check the container name against Azure's blob naming rules
before touching the scheme.
An https:// blob URL is accepted at CREATE with no credential check at all; the credential is
evaluated only at START. Expect Msg 25602 there, not Msg 25739. Msg 25739 exists and its
text is exactly this condition, "required credential for writing session output to Azure blob is
missing", and the engine does not raise it: measured 2026-09-08 on a logical server, four times,
against both a fictitious storage account and a real container with no credential, every start
answered Msg 25602 with operating system error 86, "The specified network password is not
correct". That error text sends you after a password when what is missing is the credential object.
The most common cause of Msg 25602 is documented and specific:
the database scoped credential's name must be the blob container URL itself, with no trailing
slash. Open
references/event-file-to-blob-storage.md before you
choose event_file over the ring buffer, because it carries the credential and managed identity
syntax and the table that maps each operating system error number at START to its cause.
Validation rules
- The session is created
ON DATABASE, on a connection to the user database, neverON SERVERand never againstmaster. - Every event that will be attributed or filtered by caller carries the
ACTION()clauses that attribution needs, and assertion 3 in "Check it worked" confirms the count. - The session is confirmed present in
sys.dm_xe_database_sessions, not merely insys.database_event_sessions, before an empty target is believed. - Any
durationpredicate names the event it was written for, and its unit came fromsys.dm_xe_object_columns. STARTUP_STATEis a deliberate choice, and a ring buffer is read before the session is stopped.- An
event_filetarget points at anhttps://blob URL, and access was proved by a successfulSTART, never by theCREATEsucceeding. - Started sessions and their memory were checked against the 100 session and 128 MB per database limits before another was added, and every session built by hand is dropped when it is done.
Do not
- Do not look for a
system_healthsession on Azure SQL Database. There is not one, and the time spent hunting is time nothing is being captured. - Do not read the internal
dlevent session. A large read from it can raise an out-of-memory error inmasterand affect login processing. - Do not scope a session
ON SERVERfor Azure SQL Database. The fix isON DATABASE, not a permissions escalation. - Do not grant
ALTER ANY EVENT SESSIONand expect it to work. The database-scoped permissions are theANY DATABASE EVENT SESSIONfamily. - Do not reuse a
durationthreshold across events. Microseconds and milliseconds both appear, and the wrong one fails silently in whichever direction is less noticeable. - Do not point
event_fileat a local path. It is refused atCREATEand no retry changes that. - Do not treat a successful
CREATEwith a blob target as evidence the credential works, and do not name that credential after the session or the storage account. Its name must be the container URL, and a trailing slash on it is its own failure. - Do not build a blocking diagnosis on
blocked_process_reporton Azure SQL Database. Use theBlocksdiagnostic settings category, which is the supported source for that report. - Do not restate what the captured queries, waits or blocking chains mean. That belongs to diagnose-slow-query, diagnose-blocking-and-deadlocks and diagnose-resource-pressure.
References
- references/sessions-that-capture-nothing.md when a
session runs clean and its target stays empty: the container measurements behind each case, the
captured XML, the duration column text for the events named here, and the
ACTION()comparison run side by side. - references/event-file-to-blob-storage.md before
choosing
event_fileover the ring buffer, and again ifSTARTfails: the credential and managed identity syntax, the SAS token permissions, the operating system error numbers atSTARTand what each one means, and reading.xelfiles back. - Extended Events in Azure SQL: scope, permissions, storage authorization and the resource limits quoted above. Read it before trusting any Extended Events example not written for this platform.
- Collect deadlock graphs in Azure SQL Database: the documented deadlock session, both targets, and the query that shreds a graph out of a ring buffer. Read it when the capture you need is deadlocks.
diagnose-slow-query,diagnose-blocking-and-deadlocks,diagnose-resource-pressure: what the captured data means, once this skill has produced it.read-execution-plan: the plan for a statement this session identified.