Schedule Jobs in Azure SQL Database with Azure Functions

You don’t get SQL Server Agent in Azure SQL Database. If you’ve been a DBA for any length of time, that sentence probably made you twitch. Agent is the workhorse that runs your index maintenance, collects performance baselines, purges old data, and generally keeps the lights on while you sleep. Azure SQL Database doesn’t have it, and Microsoft’s answer – Elastic Jobs – requires a separate database and comes with its own licensing.

There’s a simpler option. An Azure Function running on a timer can execute T-SQL on a schedule, costs almost nothing on a Consumption plan, and takes about fifteen minutes to set up. This post walks through building one, with both PowerShell and C# examples.

A woman DBA adjusting gears on a large clockwork mechanism connected to a glowing cloud database, with small timer displays showing cron schedules.

The Problem: Transient Data Disappears

The most common reason you need a scheduled job in Azure SQL Database is to capture data that doesn’t persist. DMV data like sys.dm_db_index_usage_stats resets whenever the service restarts – and Azure can restart your database at any time for patching, scaling, or resource rebalancing. If you’re trying to identify unused indexes, you need periodic snapshots of that data before it vanishes.

The same applies to wait statistics, query store gaps, tempdb usage patterns, and any home-grown monitoring you’ve built on top of DMVs.

The Architecture

The solution has three parts:

  1. A job table in your Azure SQL Database that stores the T-SQL commands to run, along with scheduling metadata.
  2. An Azure Function with a timer trigger that wakes up on a schedule, reads the job table, and executes each enabled job.
  3. A log table that records what ran, when, and whether it succeeded.

The Azure Function authenticates using a Managed Identity, which means no connection strings with passwords stored anywhere. The function’s identity is granted access to the database directly, just like a Windows login in on-premises SQL Server.

The Job and Log Tables

The run_interval_minutes column controls how often each job runs. The Azure Function fires every five minutes (a lightweight check), but only executes jobs whose last_run_utc is older than their configured interval. This lets you have some jobs run hourly and others daily, all from the same function.

Setting Up the Azure Function

I won’t walk through every portal click since Azure’s UI changes frequently. Here’s what you need:

  1. Create a Function App in the Azure Portal. Choose the Consumption plan (pay per execution, essentially free for a few hundred runs per day). Select either PowerShell Core or .NET as the runtime stack depending on your preference.
  2. Enable System-Assigned Managed Identity on the Function App (under Identity in the left menu).
  3. Grant the identity access to your Azure SQL Database by running the T-SQL below while connected as an admin.
  4. Create a timer-triggered function with a cron expression of 0 */5 * * * * (every 5 minutes).

Granting the Managed Identity Access

Connect to your Azure SQL Database and run:

Replace your-function-app-name with the actual name of your Function App. The identity name matches the Function App name. The roles above give it read/write access and permission to execute stored procedures. Adjust based on what your jobs actually need – principle of least privilege applies here just like it does on-premises.

Application Settings

Add one application setting to your Function App:

No username or password. The Authentication=Active Directory Managed Identity parameter tells the SQL client library to use the function’s managed identity automatically.

The Azure Function: PowerShell Version

The Azure Function: C# Version

For the C# version, create a .NET 8 (or later) isolated worker function. You’ll need the Microsoft.Data.SqlClient and Azure.Identity NuGet packages.

The C# version uses parameterized queries for the logging insert, which avoids SQL injection if a job’s error message contains single quotes. The PowerShell version uses string escaping for simplicity, but you could adapt it to use parameters as well.

Example Job: Index Usage Stats Collector

Here’s the T-SQL to insert a job that snapshots sys.dm_db_index_usage_stats every hour. First, create the history table:

Then register the collection job:

After a few weeks of hourly snapshots, you can query the history to find genuinely unused indexes:

The HAVING clause filters to indexes with zero seeks, scans, and lookups across all snapshots. The ORDER BY on user_updates shows you the most expensive unused indexes first – these are costing you write performance for zero read benefit.

Operational Considerations

Cost. A Consumption-plan Azure Function that runs every five minutes and executes a few queries costs pennies per month. You’ll pay for the Function App storage account (a few cents) and the executions themselves. For this workload, you’re unlikely to exceed the free grant of 1 million executions per month.

Timeouts. Consumption plan functions have a 10-minute timeout by default (configurable up to 10 minutes). If you have long-running maintenance jobs, consider the Flex Consumption or App Service plan, which support longer timeouts. The 300-second CommandTimeout in the code above is a safety net for individual queries.

Monitoring. Azure Functions integrate with Application Insights out of the box. Enable it when creating the Function App and you get execution logs, failure alerts, and performance metrics without any extra code.

Error handling. The function logs failures to the ScheduledJobLog table and continues to the next job. A single failing job won’t block the others. Set up an alert in Application Insights for failed executions if you want email notifications.

When to use Elastic Jobs instead. If you need to run the same job across multiple databases (fan-out), or if you need built-in retry policies, job dependencies, or the ability to run jobs against databases in different servers, Elastic Jobs is the right tool. The Azure Function approach is better for single-database scheduling where you want simplicity and low cost.

Wrapping Up

The combination of an Azure Function, a Managed Identity, and a simple job table gives you a lightweight, secure, and nearly free replacement for SQL Server Agent’s scheduling capability in Azure SQL Database. The five-minute polling interval is more than sufficient for most DBA maintenance tasks, and you can tune it per-job through the run_interval_minutes column.

The index usage stats collector is just one example. Other good candidates for scheduled collection: wait statistics snapshots, query store forced plan validation, data retention cleanup, and statistics maintenance on tables where auto-stats isn’t keeping up.

Have questions or a use case I didn’t cover? Find me on Bluesky or LinkedIn.