As a SQL Server DBA, especially when working in production or operational environments, you’ll eventually run into SQL Agent jobs that run longer than they should. Maybe they exceed your SLAs or start affecting other jobs, applications, or users. Tools like the Job Activity Monitor in SQL Server Management Studio (SSMS) are great for keeping an eye on things manually, but sometimes you need automation, something that can alert you right away when a job runs past a certain threshold.
In this post, I’ll share a simple and effective T-SQL script that helps you track and get alerts for long-running SQL Agent jobs. It’s easy to plug into your environment, doesn’t depend on any third-party tools, and can be tailored to fit your organization’s needs. Even if you already have access to a full suite of monitoring tools, I still find this method incredibly handy because it’s straightforward, flexible, and quick to adapt wherever and whenever you need it.
Script Overview
The script is divided into two main parts:
Part 1 identifies active/running jobs whose run durations exceed a set threshold.
Part 2 formats the results and sends an email alert with the job details.
You can execute the whole script at once or run each part separately based on your workflow.
Setup Instructions
Configure Database Mail if not already enabled.
Update the variables at the top of the script, especially the runtime threshold (in minutes) and email address.
Schedule this script as a SQL Agent job or run manually as required.
Review output in SSMS or wait for the automated email alert.
TSQL Script:
-- TESTED ON SQL SERVER 2016 AND UP ------------
-- Part 1: Identify long-running jobs SET NOCOUNT ON; -- Set the alert threshold values below -- You can set either minutes or seconds or both as a threshold -- Max up to 35791 minutes can be specified or we will get Arithmetic overflow error DECLARE @ThresholdMinutes INT = 30; -- Change as needed, max up to 35791 minutes DECLARE @ThresholdSeconds INT = 1; -- Change as needed SET @ThresholdMinutes = ISNULL(@ThresholdMinutes, 0); SET @ThresholdSeconds = ISNULL(@ThresholdSeconds, 0); DECLARE @RecipientEmail NVARCHAR(400) = '<EmailAddress>'; -- Add actual recipient email -- Convert Threshold value into Milliseconds DECLARE @ThresholdMilliseconds INT ; SET @ThresholdMilliseconds = (@ThresholdSeconds * 1000) + (@ThresholdMinutes * 1000 * 60); IF OBJECT_ID('tempdb..#job_durations') IS NOT NULL DROP TABLE #job_durations; ;WITH cte_jobs AS ( SELECT sja.session_id AS [Session ID], sj.name AS [Job Name], r.command AS [Command], [Status] = CASE WHEN sja.start_execution_date IS NULL THEN 'Not running' WHEN sja.start_execution_date IS NOT NULL AND sja.stop_execution_date IS NULL THEN 'Running' ELSE 'Not running' END, sja.start_execution_date AS [Job Start Time], r.total_elapsed_time AS [Elapsed Milliseconds], r.total_elapsed_time / 1000 AS [Elapsed Seconds], r.total_elapsed_time / 1000 / 60 AS [Elapsed Minutes], r.total_elapsed_time / 1000 / 60 / 60 AS [Elapsed Hours], sj.job_id AS [Job ID] FROM msdb..sysjobs sj INNER JOIN msdb..sysjobactivity sja ON sj.job_id = sja.job_id INNER JOIN ( SELECT TRY_CONVERT(binary(30), SUBSTRING(program_name, 30, 34), 1) AS job_id, * FROM sys.dm_exec_sessions WHERE program_name LIKE 'SQLAgent - TSQL JobStep%' ) s ON sj.job_id = s.job_id LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id WHERE sja.session_id = (SELECT MAX(session_id) FROM msdb..sysjobactivity WHERE job_id = sj.job_id) ) SELECT cte.*, [Duration] = CASE WHEN [Elapsed Hours] > 0 THEN CAST([Elapsed Hours] AS VARCHAR(20)) + ' Hours' ELSE '' END + CASE WHEN [Elapsed Minutes] > 0 AND [Elapsed Hours] = 0 THEN CAST([Elapsed Minutes] AS VARCHAR(20)) + ' Minutes' ELSE '' END + CASE WHEN [Elapsed Minutes] > 0 AND [Elapsed Hours] > 0 THEN ', ' + CAST([Elapsed Minutes] - ([Elapsed Hours] * 60) AS VARCHAR(20)) + ' Minutes' ELSE '' END + CASE WHEN [Elapsed Seconds] > 0 AND [Elapsed Minutes] = 0 AND [Elapsed Hours] = 0 THEN CAST([Elapsed Seconds] AS VARCHAR(20)) + ' Seconds' ELSE '' END + CASE WHEN [Elapsed Seconds] > 0 AND [Elapsed Minutes] > 0 THEN ', ' + CAST([Elapsed Seconds] - ([Elapsed Minutes] * 60) AS VARCHAR(20)) + ' Seconds' ELSE '' END INTO #job_durations FROM cte_jobs cte WHERE 1 = 1 AND [Status] = 'Running' and [Elapsed Milliseconds] > @ThresholdMilliseconds SELECT [Session ID], [Job Name], [Command], [Job Start Time], [Duration], [Elapsed Milliseconds] FROM #job_durations ORDER BY [Elapsed Milliseconds] DESC; IF @@ROWCOUNT = 0 GOTO QUIT; -- Part 2: Email alerts for long-running jobs DECLARE @html_body NVARCHAR(MAX), @html_table_head VARCHAR(1000), @html_table_tail VARCHAR(1000); SET @html_table_head = '<html><head>' + '<style>td {border: solid black; border-width: 1px; padding: 5px; font: 15px arial;}</style>' + '</head><body>' + '<pre style="font-weight: bold">SQL Server Instance: ' + @@SERVERNAME + '</pre>' + -- '<br>Alert Threshold: ' + CAST(@ThresholdMilliseconds AS VARCHAR(100)) + '<br><br>Report generated on: ' + CAST(GETDATE() AS VARCHAR(100)) + '<br><table cellpadding="0" cellspacing="0" border="0">' + '<tr><td bgcolor="#E6E6FA"><b>Job Name</b></td>' + '<td bgcolor="#E6E6FA"><b>Duration</b></td>' + '<td bgcolor="#E6E6FA"><b>Command</b></td></tr>'; SET @html_table_tail = '</table></body></html>'; SET @html_body = ( SELECT td = LEFT([Job Name], 50), '', td = [Duration], '', td = [Command], '' FROM #job_durations ORDER BY [Elapsed Milliseconds] DESC FOR XML RAW('tr'), ELEMENTS ); SELECT @html_body = @html_table_head + ISNULL(@html_body, '') + @html_table_tail; EXEC msdb.dbo.sp_send_dbmail -- @profile_name = 'DBMail_DBA', -- Uncomment and edit if using a named mail profile @recipients = @RecipientEmail, @body = @html_body, @subject = 'Long Running SQL Server Jobs Found', @body_format = 'HTML'; QUIT:
Example Output
For a demo, I started two sample jobs, doing nothing but running the WAITFOR DELAY command. Here is the results displayed on the screen:
If a job exceeds your chosen threshold, you’ll receive an HTML-formatted email alert summarizing job details:
You can adapt thresholds and recipients to suit your production environment or integrate further enhancements as needed.
Tips
Customize thresholds at the top of the script for reuse and clarity.
Add exclusions or filters for specific job names as needed.
Provide context for others: update email subjects, include server or environment tags.
Enhance with automation: Schedule this script as a recurring SQL Agent job.
Extend for more detail: Add columns for the affected database, more job properties, or job step text.
Conclusion
My hope is that this script gives DBAs a quick, flexible way to keep tabs on long-running SQL Server Agent jobs - no third-party tools required. With just a few tweaks, it can scale nicely for large enterprise setups or work just as well in smaller environments. Feel free to adapt it to your needs, and if you come up with improvements or new ideas, share them in the comments!
Exercise: Extending to Long-Running Query Monitoring
If you want to take things a step further, you can easily tweak this script’s logic to keep an eye on long-running queries instead of just jobs. By querying dynamic management views like sys.dm_exec_requests and sys.dm_exec_sessions, you can spot active queries that go over a set duration. Grab details like the start_time and total_elapsed_time, join in the actual query text with sys.dm_exec_sql_text, and you’ll be able to create a similar alert system for queries that might be dragging down performance. It’s a great way to dive deeper into SQL Server DMVs and start building your own custom monitoring solutions that go beyond just tracking jobs.