Erik Darling and Kendra Little Talk AI, Databases, and SQL Server 2025
Erik Darling joins me on the Dear SQL DBA Podcast to chat about AI tools, why they work better for Python and PowerShell than SQL, and what’s …
Read MoreThe rpc_completed event in Extended Events is useful when troubleshooting SQL Server performance. It captures detailed information about Remote Procedure Calls: that means stored procedure executions, including the calls to sp_executesql often used by applications (including Entity Framework) to run parameterized queries against SQL Server. The output for rpc_completed includes the parameters that were specified along with values provided, and the CPU time, logical reads, and duration used by the query.
It can be frustrating to figure out how to filter this in Extended Events. Struggling with this is one of the primary reasons I sometimes use ye Olde Profiler for initial investigations and to speedily observe something in SQL Server.
Here is my survival guide to filtering rpc_completed, which makes using XEvents suck less.

As of this writing, they aren’t great at scripting Extended Event sessions with working filters. As I’ll dig into below, a lot of details of Extended Events aren’t documented clearly, the user is left to query them from a SQL Server instance. LLMs do what they can from published walkthroughs and StackOverflow answers, but they end up trying to use fields and filters for rpc_completed from other events which don’t work. They also tend to hallucinate things like whether you can filter on a simple field for schema name on rpc_completed. (You can’t.)
Possibly this post will make them a bit better by giving them more examples to scrape and work with. Good luck, robots.
If you want a simpler way to create Extended Events sessions with sensible defaults and filtering, check out Erik Darling’s sp_HumanEvents, a stored procedure that creates Extended Events sessions for common troubleshooting scenarios.
The rpc_completed event has a key advantage over sql_batch_completed: it always captures parameter values for stored procedures and parameterized queries.
This makes it more useful when you want to set up code to reproduce a problem. It also makes it useful for troubleshooting parameter sniffing problems, where you need to see which specific parameter values are being passed in to run queries, as this isn’t captured in Query Store.
The rpc_completed event captures several useful data fields automatically as part of the event payload.
You can query the available fields for an event with TSQL and the event name, which is handy because there isn’t a docs page for the properties of each Extended Event. Or maybe it’s why there isn’t a docs page? The software is supposed to be self-documenting, I guess.
SELECT
oc.name,
oc.description,
oc.column_type
FROM sys.dm_xe_object_columns AS oc
WHERE oc.object_name = N'rpc_completed'
AND oc.column_type = N'data'
ORDER BY
oc.name;
GO
Useful data fields for rpc_completed include the following. These are the descriptions returned for these by SQL Server 2025:
These fields are automatically captured. To get additional context like the database name or client application name, add those as actions in the EVENT definition. (Examples are below.)
event_time is also automatically available. This will always be in the UTC time zone, even if you have the timezone for the SQL Server Instance set to something else. I always say that East Coast time is best coast time, but I guess someone got tired of dealing with Daylight Savings Time, and who can blame them.
rpc_completed events fire for:
sp_executesql with parameters. You’ll take my dynamic SQL from my cold, dead hands. Also commonly used by .NET applications.sp_prepare/sp_execute (more common with ODBC and JDBC than .NET, which typically uses sp_executesql for parameterized queries)rpc_completed events. These are run as T-SQL batches, even if you run a stored procedure in the T-SQL batch.Let’s create a basic Extended Events session to capture all RPC calls.
Even in this simple example, I’m filtering out system database and system session calls as a sensible default. (If you need those, you can delete that whole WHERE clause.)
CREATE EVENT SESSION [capture_rpc_calls]
ON SERVER
ADD EVENT sqlserver.rpc_completed
(
ACTION
(
sqlserver.client_app_name,
sqlserver.database_name,
sqlserver.username
)
WHERE
(
[package0].[greater_than_uint64]([sqlserver].[database_id], (4)) /* Exclude master, model, msdb, tempdb */
AND [package0].[equal_boolean]([sqlserver].[is_system], (0)) /* Exclude system sessions */
)
)
ADD TARGET package0.event_file
(
SET filename = N'C:\Temp\capture_rpc_calls.xel',
max_file_size = 50,
max_rollover_files = 5
)
WITH
(
MAX_MEMORY=4096 KB,
MAX_DISPATCH_LATENCY=30 SECONDS,
EVENT_RETENTION_MODE=ALLOW_MULTIPLE_EVENT_LOSS
);
GO
ALTER EVENT SESSION [capture_rpc_calls]
ON SERVER
STATE = START;
GO
Let’s also create a stored procedure in SSMS. I’m using the StackOverflow2013 sample database.
USE StackOverflow2013;
GO
/* Create a simple test procedure */
CREATE OR ALTER PROCEDURE dbo.TestProc
@UserId integer,
@StartDate datetime
AS
BEGIN
SELECT
PostCount = COUNT(*)
FROM dbo.Posts AS p
WHERE p.OwnerUserId = @UserId
AND p.CreationDate >= @StartDate;
END;
GO
With those in place, let’s generate some rpc_completed events, and also run a query that should not generate an rpc_completed event.
Save this PowerShell script as Test-RpcCompletedEvents.ps1:
# Test script to generate RPC completed events for Extended Events testing
param(
[string]$ServerInstance = "localhost\FABRIC",
[string]$Database = "StackOverflow2013"
)
Write-Host "Connecting to $ServerInstance, database $Database..." -ForegroundColor Cyan
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = "Server=$ServerInstance;Database=$Database;Integrated Security=True;"
try {
$connection.Open()
Write-Host "Connected successfully." -ForegroundColor Green
# This stored procedure call generates an rpc_completed event
Write-Host "`n1. Executing stored procedure (generates rpc_completed event)..." -ForegroundColor Yellow
$command = $connection.CreateCommand()
$command.CommandType = [System.Data.CommandType]::StoredProcedure
$command.CommandText = "dbo.TestProc"
$command.Parameters.AddWithValue("@UserId", 22656) | Out-Null
$command.Parameters.AddWithValue("@StartDate", "2013-01-01") | Out-Null
$command.ExecuteNonQuery()
Write-Host " Stored procedure executed." -ForegroundColor Green
# This also generates an rpc_completed event (sp_executesql)
Write-Host "`n2. Executing parameterized query (generates rpc_completed event via sp_executesql)..." -ForegroundColor Yellow
$command2 = $connection.CreateCommand()
$command2.CommandType = [System.Data.CommandType]::Text
$command2.CommandText = "SELECT PostCount = COUNT(*) FROM dbo.Posts AS p WHERE p.OwnerUserId = @UserId AND p.CreationDate >= @StartDate"
$command2.Parameters.AddWithValue("@UserId", 22656) | Out-Null
$command2.Parameters.AddWithValue("@StartDate", "2013-01-01") | Out-Null
$command2.ExecuteNonQuery()
Write-Host " Parameterized query executed." -ForegroundColor Green
# This does NOT generate an rpc_completed event (T-SQL batch, not RPC)
Write-Host "`n3. Executing T-SQL batch (does NOT generate rpc_completed event)..." -ForegroundColor Yellow
$command3 = $connection.CreateCommand()
$command3.CommandType = [System.Data.CommandType]::Text
$command3.CommandText = "SELECT YouDoNotSeeMeInYourTrace = COUNT(*) FROM dbo.Posts AS p WHERE p.OwnerUserId = 22656 AND p.CreationDate >= '2013-01-01'"
$command3.ExecuteNonQuery()
Write-Host " T-SQL batch executed." -ForegroundColor Green
# Additional test: sp_executesql with Users table (for the dynamic SQL filter example)
Write-Host "`n4. Executing parameterized query against Users table (for dynamic SQL filter testing)..." -ForegroundColor Yellow
$command4 = $connection.CreateCommand()
$command4.CommandType = [System.Data.CommandType]::Text
$command4.CommandText = "SELECT u.Id, u.DisplayName, u.Reputation FROM dbo.Users AS u WHERE u.Location = @Location"
$command4.Parameters.AddWithValue("@Location", "New York, NY") | Out-Null
$command4.ExecuteNonQuery()
Write-Host " Users query executed." -ForegroundColor Green
Write-Host "`nAll test queries completed!" -ForegroundColor Cyan
Write-Host "Note: Extended Events may take up to 30 seconds to flush events to file (MAX_DISPATCH_LATENCY)." -ForegroundColor Yellow
}
catch {
Write-Host "Error: $_" -ForegroundColor Red
throw
}
finally {
if ($connection.State -eq 'Open') {
$connection.Close()
Write-Host "`nConnection closed." -ForegroundColor Cyan
}
}
The script executes:
rpc_completed event)sp_executesql (generates rpc_completed event)rpc_completed event)Run it with:
# With default values (localhost\FABRIC and StackOverflow2013):
.\Test-RpcCompletedEvents.ps1
# Or specify your server and database:
.\Test-RpcCompletedEvents.ps1 -ServerInstance "YourServer\YourInstance" -Database "YourDatabase"
Read from the trace file to verify what was captured. We set MAX_DISPATCH_LATENCY to 30 seconds in our XEvents trace definition (which is also the default), so it may take up that amount of time for data to be flushed to the file. (Docs on CREATE EVENT SESSION)
/* Read and parse the events from the file */
SELECT
event_time = event_data.value('(event/@timestamp)[1]', 'datetime2'),
database_name = event_data.value('(event/action[@name="database_name"]/value)[1]', 'nvarchar(128)'),
username = event_data.value('(event/action[@name="username"]/value)[1]', 'nvarchar(128)'),
object_name = event_data.value('(event/data[@name="object_name"]/value)[1]', 'nvarchar(128)'),
statement = event_data.value('(event/data[@name="statement"]/value)[1]', 'nvarchar(max)'),
duration_ms = event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') / 1000,
cpu_time_ms = event_data.value('(event/data[@name="cpu_time"]/value)[1]', 'bigint') / 1000,
logical_reads = event_data.value('(event/data[@name="logical_reads"]/value)[1]', 'bigint'),
row_count = event_data.value('(event/data[@name="row_count"]/value)[1]', 'bigint')
FROM
(
SELECT
event_data = CAST(event_data AS xml)
FROM sys.fn_xe_file_target_read_file
(
N'C:\Temp\capture_rpc_calls*.xel',
NULL,
NULL,
NULL
)
) AS xe;
GO
The results show:

The statements for calls to sp_executesql were captured with the parameter values. For example:
exec sp_executesql N'SELECT PostCount = COUNT(*) FROM dbo.Posts AS p WHERE p.OwnerUserId = @UserId AND p.CreationDate >= @StartDate',N'@UserId int,@StartDate nvarchar(10)',@UserId=22656,@StartDate=N'2013-01-01'
You usually don’t want to capture every RPC call. Filtering to just capture what you need makes the trace lighter, and also your life easier going through the trace when problem solving.
Filtering by object name and by the login executing the commands is very useful:
CREATE EVENT SESSION [capture_specific_proc]
ON SERVER
ADD EVENT sqlserver.rpc_completed
(
ACTION
(
sqlserver.client_app_name,
sqlserver.database_name,
sqlserver.username
)
WHERE
(
[object_name] = N'TestProc'
AND [package0].[greater_than_uint64]([sqlserver].[database_id], (4)) /* Exclude master, model, msdb, tempdb */
AND [package0].[equal_boolean]([sqlserver].[is_system], (0)) /* Exclude system sessions */
/* Uncomment to filter by SQL login */
/* AND [sqlserver].[username] = N'SQLLoginName' */
/* Uncomment to filter by Windows login */
/* AND [sqlserver].[username] = N'DOMAIN\WindowsUser' */
)
)
ADD TARGET package0.event_file
(
SET filename = N'C:\Temp\capture_specific_proc.xel',
max_file_size = 50,
max_rollover_files = 5
)
WITH
(
MAX_MEMORY=4096 KB,
MAX_DISPATCH_LATENCY=30 SECONDS,
EVENT_RETENTION_MODE=ALLOW_MULTIPLE_EVENT_LOSS
);
GO
ALTER EVENT SESSION [capture_specific_proc]
ON SERVER
STATE = START;
GO
The object_name predicate matches the procedure name, but without the schema.
The example above uses an equality. To pattern match with a case-insensitive comparison, use the like_i_sql_unicode_string predicate operator instead:
[sqlserver].[like_i_sql_unicode_string]([object_name], N'test%')
Rerunning Test-RpcCompletedEvents.ps1 and then the query to read from the file (adapted for this trace file name), this captured only the call to TestProc:

You can combine object_name and statement filters for more precise targeting. This is useful when you want to ensure you’re capturing a specific procedure and also verify it’s being called in a particular way.
This is also useful because there is not a schema_name field to filter on.
The following example only works if the exact statement being issued leads with exec dbo.TestProc.
CREATE EVENT SESSION [capture_testproc_with_sqltext]
ON SERVER
ADD EVENT sqlserver.rpc_completed
(
ACTION
(
sqlserver.client_app_name,
sqlserver.database_name,
sqlserver.username
)
WHERE
(
[object_name] = N'TestProc'
AND [sqlserver].[like_i_sql_unicode_string]([statement], N'exec dbo.TestProc%')
AND [package0].[greater_than_uint64]([sqlserver].[database_id], (4)) /* Exclude master, model, msdb, tempdb */
AND [package0].[equal_boolean]([sqlserver].[is_system], (0)) /* Exclude system sessions */
/* Uncomment to filter by SQL login */
/* AND [sqlserver].[username] = N'SQLLoginName' */
/* Uncomment to filter by Windows login */
/* AND [sqlserver].[username] = N'DOMAIN\WindowsUser' */
)
)
ADD TARGET package0.event_file
(
SET filename = N'C:\Temp\capture_testproc_with_sqltext.xel',
max_file_size = 50,
max_rollover_files = 5
)
WITH
(
MAX_MEMORY=4096 KB,
MAX_DISPATCH_LATENCY=30 SECONDS,
EVENT_RETENTION_MODE=ALLOW_MULTIPLE_EVENT_LOSS
);
GO
ALTER EVENT SESSION [capture_testproc_with_sqltext]
ON SERVER
STATE = START;
GO
I tested this to make sure it works, but the output is the same as above: it captures dbo.TestProc.
It’s common for applications to use sp_executesql to execute parameterized queries against SQL Server.
In this case, the object_name will be sp_executesql. To filter for specific dynamic SQL, you need to filter on the statement data field, which contains the actual SQL statement being executed.
CREATE EVENT SESSION [capture_specific_dynamic_sql]
ON SERVER
ADD EVENT sqlserver.rpc_completed
(
ACTION
(
sqlserver.client_app_name,
sqlserver.database_name,
sqlserver.username
)
WHERE
(
[object_name] = N'sp_executesql'
AND [sqlserver].[like_i_sql_unicode_string]([statement], N'%FROM dbo.Users%')
AND [package0].[greater_than_uint64]([sqlserver].[database_id], (4)) /* Exclude master, model, msdb, tempdb */
AND [package0].[equal_boolean]([sqlserver].[is_system], (0)) /* Exclude system sessions */
/* Uncomment to filter by SQL login */
/* AND [sqlserver].[username] = N'SQLLoginName' */
/* Uncomment to filter by Windows login */
/* AND [sqlserver].[username] = N'DOMAIN\WindowsUser' */
)
)
ADD TARGET package0.event_file
(
SET filename = N'C:\Temp\capture_specific_dynamic_sql.xel',
max_file_size = 50,
max_rollover_files = 5
)
WITH
(
MAX_MEMORY=4096 KB,
MAX_DISPATCH_LATENCY=30 SECONDS,
EVENT_RETENTION_MODE=ALLOW_MULTIPLE_EVENT_LOSS
);
GO
ALTER EVENT SESSION [capture_specific_dynamic_sql]
ON SERVER
STATE = START;
GO
It’s a match!

You may want to only capture queries of a certain duration or that do more than a given amount of IO.
CREATE EVENT SESSION [capture_by_duration_reads]
ON SERVER
ADD EVENT sqlserver.rpc_completed
(
ACTION
(
sqlserver.client_app_name,
sqlserver.database_name,
sqlserver.username
)
WHERE
(
[sqlserver].[database_name] = N'StackOverflow2013'
AND (
[object_name] = N'TestProc'
OR [object_name] = N'UpdateUserReputation'
OR [object_name] = N'GetTopUsers'
)
AND (
[duration] > 5000000 /* More than 5 seconds (in microseconds) */
OR [logical_reads] > 10000 /* More than 10,000 logical reads */
)
AND [package0].[greater_than_uint64]([sqlserver].[database_id], (4)) /* Exclude master, model, msdb, tempdb */
AND [package0].[equal_boolean]([sqlserver].[is_system], (0)) /* Exclude system sessions */
/* Uncomment to filter by SQL login */
/* AND [sqlserver].[username] = N'SQLLoginName' */
/* Uncomment to filter by Windows login */
/* AND [sqlserver].[username] = N'DOMAIN\WindowsUser' */
)
)
ADD TARGET package0.event_file
(
SET filename = N'C:\Temp\capture_by_duration_reads.xel',
max_file_size = 50,
max_rollover_files = 5
)
WITH
(
MAX_MEMORY=4096 KB,
MAX_DISPATCH_LATENCY=30 SECONDS,
EVENT_RETENTION_MODE=ALLOW_MULTIPLE_EVENT_LOSS
);
GO
ALTER EVENT SESSION [capture_by_duration_reads]
ON SERVER
STATE = START;
GO
This session will only capture calls that meet your thresholds. One event matches the criteria:

This stops and drops all Extended Events sessions created in this post (and any others that begin with ‘capture’)
/* Stop and drop all sessions named like capture% */
DECLARE
@session_name nvarchar(128);
DECLARE session_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT
ses.name
FROM sys.server_event_sessions AS ses
WHERE ses.name like N'capture%';
OPEN session_cursor;
FETCH NEXT FROM session_cursor
INTO @session_name;
WHILE @@FETCH_STATUS = 0
BEGIN
/* Stop the session if it's running */
DECLARE
@stop_sql nvarchar(max) = N'
ALTER EVENT SESSION ' + QUOTENAME(@session_name) + N'
ON SERVER
STATE = STOP;';
BEGIN TRY
EXECUTE sp_executesql @stop_sql;
END TRY
BEGIN CATCH
/* Session might not be running, continue */
END CATCH;
/* Drop the session */
DECLARE
@drop_sql nvarchar(max) = N'
DROP EVENT SESSION ' + QUOTENAME(@session_name) + N'
ON SERVER;';
EXECUTE sp_executesql @drop_sql;
FETCH NEXT FROM session_cursor
INTO @session_name;
END;
CLOSE session_cursor;
DEALLOCATE session_cursor;
GO
/* Verify all sessions are removed */
SELECT
ses.name
FROM sys.server_event_sessions AS ses
WHERE ses.name LIKE N'capture%';
GO
When filtering rpc_completed events…
Start specific:
[sqlserver].[username] running the query is usually a good thingobject_name if troubleshooting a known procedure[object_name] = N'TestProc'[sqlserver].[like_i_sql_unicode_string]([object_name], N'test%')object_name will usually be sp_executesql, so filter by [object_name] = N'sp_executesql' and add filters on the statement field to narrow down to specific queriesdatabase_name to avoid capturing the same procedure name in different databases[sqlserver].[database_name] = N'StackOverflow2013'[package0].[greater_than_uint64]([sqlserver].[database_id], (4))[package0].[equal_boolean]([sqlserver].[is_system], (0))You can filter on schema name, but only in the statement: There is no schema name field to filter on for rpc_completed events. If you need to filter by schema, you can filter on the statement field using [sqlserver].[like_i_sql_unicode_string]([statement], N'exec dbo.TestProc%') to match schema-qualified procedure calls. This approach depends on how the application calls the procedure, so you’ll need to take samples first to narrow this down.
Combine object and performance filters: Filter by object name AND duration to catch only the slow executions for RPC events. You can also filter by logical_reads to exclude calls that do a minimum of IO. For parameterized queries from applications, combine [object_name] = N'sp_executesql' with performance filters like [duration] > 5000000 to capture expensive parameterized queries without capturing every single execution.
Test filters in development first: Create the session in a dev environment to verify your filters capture what you expect before deploying to production. Remember that queries run from SSMS do not fire rpc_completed events (they’re T-SQL batches, not RPC calls), so use the PowerShell script or an application connection to test your filters.
Sample by session_id for high-volume scenarios: You can sample a percentage of sessions using the divides_by_uint64 function on session_id. The example sampling traces in the XEvents tooling suggest this technique:
WHERE (
[package0].[divides_by_uint64]([sqlserver].[session_id], (5)) /* Sample 20% of sessions (every 5th session) */
)
Adjust the divisor in divides_by_uint64 to change the sampling rate: use 10 for 10% sampling, 20 for 5% sampling, etc.
Without filtering, XEvents traces often generate an overwhelming amount of data.
The filtering techniques in this post (object name, SQL text matching, performance thresholds, and session sampling) focus on the events that matter. Filtering by specific procedures, expensive queries, or a sample of sessions captures the signal needed for troubleshooting without extraneous noise.
Copyright (c) 2025, Catalyze SQL, LLC; all rights reserved. Opinions expressed on this site are solely those of Kendra Little of Catalyze SQL, LLC. Content policy: Short excerpts of blog posts (3 sentences) may be republished, but longer excerpts and artwork cannot be shared without explicit permission.