How to Filter RPC_COMPLETED Events in Extended Events: SQL Server Guide

How to Filter RPC_COMPLETED Events in Extended Events: SQL Server Guide

How to Filter RPC_COMPLETED Events in Extended Events: SQL Server Guide 15 min read
How to Filter RPC_COMPLETED Events in Extended Events: SQL Server Guide

The 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.

Surely ChatGPT, Claude, or Cursor Make Scripting this Easier?

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.

You Can Save Time by Using sp_HumanEvents for Extended Events

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.

Why RPC_COMPLETED Events Are Useful for Troubleshooting

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.

Data Fields in the rpc_completed Event

The rpc_completed event captures several useful data fields automatically as part of the event payload.

🔥 Beware: There are docs pages for Profiler events, like this one for RPC:Completed Event Class, but the column list and their names for Profiler Event Classes are different than what you get in Extended Events. Using this documentation to write XEvents scripts is a gotcha that will waste time.

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:

  • statement: The text of the statement that was run by the remote procedure call.
  • cpu_time: The CPU time (in microseconds) used by the remote procedure call.
  • duration: The time (in microseconds) that the remote procedure call took to be completed.
  • logical_reads: The number of logical page reads that were issued by the remote procedure call. These are all 8KB pages if you want to translate into GB read. Logical page reads include physical page reads.
  • physical_reads: The number of physical page reads that were issued by the remote procedure call.
  • writes: The number of page writes that were issued by the remote procedure call.
  • row_count: The number of rows that were returned by the remote procedure call.
  • result: The return value from the remote procedure call.

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.

What Sends RPC Completed Events

rpc_completed events fire for:

  1. Stored procedure calls: When an application or client executes a stored procedure (using the RPC protocol).
  2. Parameterized queries: When using sp_executesql with parameters. You’ll take my dynamic SQL from my cold, dead hands. Also commonly used by .NET applications.
  3. Prepared statement execution: When applications use prepared statements via sp_prepare/sp_execute (more common with ODBC and JDBC than .NET, which typically uses sp_executesql for parameterized queries)
🔥 Common error: Queries you run from a SQL Server Management Studio session do not fire rpc_completed events. These are run as T-SQL batches, even if you run a stored procedure in the T-SQL batch.

For this reason, we'll use PowerShell to test firing RPC events in the code below.

How to Test RPC Completed Events

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:

  1. A stored procedure call (generates rpc_completed event)
  2. A parameterized query via sp_executesql (generates rpc_completed event)
  3. A T-SQL batch without parameters (does NOT generate rpc_completed event)
  4. An additional parameterized query against the Users table (for testing dynamic SQL filters)

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:

  • Stored procedure executions were captured, including sp_executesql calls (RPC calls)
  • The ad-hoc SELECT was not (language event)

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'

Filter by Object Name

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:

Filter by Object Name and SQL Text

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.

Filter for a Specific Query Called by sp_executesql

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!

Filter by Duration or Logical Reads

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:

Extended Events Session Cleanup Script

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

RPC_COMPLETED Filtering Strategies

When filtering rpc_completed events…

  1. Start specific:

    • Filtering by the [sqlserver].[username] running the query is usually a good thing
    • Filter by object_name if troubleshooting a known procedure
      • Equality example: [object_name] = N'TestProc'
      • Like example: [sqlserver].[like_i_sql_unicode_string]([object_name], N'test%')
    • For parameterized queries sent from applications (like Entity Framework), the 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 queries
    • Filter on database_name to avoid capturing the same procedure name in different databases
      • Equality example: [sqlserver].[database_name] = N'StackOverflow2013'
    • Otherwise at least filter out system databases if you can with [package0].[greater_than_uint64]([sqlserver].[database_id], (4))
    • Filter out system activity with [package0].[equal_boolean]([sqlserver].[is_system], (0))
  2. 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.

  3. 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.

  4. 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.

  5. 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.

Filtering Best Practices: Capture the Signal, Not the Noise

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.