Skip to main content Skip to footer

Giving Umbraco 17 A Bigger Clock: Raising The SQL Command Timeout With A Composer

Every now and then a backoffice action in Umbraco needs to talk to the database for longer than ADO.NET is willing to wait. The default CommandTimeout on a SqlCommand is 30 seconds, and when you blow past it you get the familiar:

Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.

I recently hit this on a clients site when upgrading to Umbraco 17.3. Rather than sprinkle CommandTimeout overrides around every call site, I raised the timeout globally by decorating Umbraco's IUmbracoDatabaseFactory. This post walks through how.

The problem

Umbraco builds its IUmbracoDatabase instances through IUmbracoDatabaseFactory. When a database is created from that factory, CommandTimeout is left at the NPoco / ADO.NET default of 30 seconds. That's fine for 99% of backoffice traffic, content saves, tree loads, property lookups, but it's not enough for the occasional heavy query, migration, or maintenance task I need to run through the Umbraco connection.

The timeout is a per-command setting on IUmbracoDatabase. I wanted a single, centralised place to raise it so every database the factory hands out picks up the new value.

The tool: a composer and a decorator

Umbraco's composition pipeline is the natural place for this. A composer runs during startup and gets to mutate the IUmbracoBuilder/IServiceCollection before the container is built. That lets me use Decorate<T> (from Microsoft.Extensions.DependencyInjection via Scrutor, which Umbraco ships with) to wrap the built-in factory with my own.

Here's what I added:

Composers/IncreaseCommandTimeoutComposer.cs:

using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.Persistence;

public class IncreaseCommandTimeoutComposer : IComposer
{
    private const int CommandTimeoutSeconds = 1800;

    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.Decorate(
            inner => new TimeoutSettingDatabaseFactory(inner, CommandTimeoutSeconds));
    }
}

internal sealed class TimeoutSettingDatabaseFactory : IUmbracoDatabaseFactory
{
    private readonly IUmbracoDatabaseFactory _inner;
    private readonly int _commandTimeout;

    public TimeoutSettingDatabaseFactory(IUmbracoDatabaseFactory inner, int commandTimeout)
    {
        _inner = inner;
        _commandTimeout = commandTimeout;
    }

    public bool Configured => _inner.Configured;
    public bool Initialized => _inner.Initialized;
    public bool CanConnect => _inner.CanConnect;
    public string ConnectionString => _inner.ConnectionString;
    public string ProviderName => _inner.ProviderName;
    public ISqlContext SqlContext => _inner.SqlContext;
    public IBulkSqlInsertProvider BulkSqlInsertProvider => _inner.BulkSqlInsertProvider;

    public void Configure(ConnectionStrings connectionStrings) => _inner.Configure(connectionStrings);
    public void ConfigureForUpgrade() => _inner.ConfigureForUpgrade();

    public IUmbracoDatabase CreateDatabase()
    {
        var db = _inner.CreateDatabase();
        db.CommandTimeout = _commandTimeout;
        return db;
    }

    public void Dispose() => _inner.Dispose();
}

Two things are going on here.

1. The composer

IncreaseCommandTimeoutComposer implements IComposer, so Umbraco's type finder picks it up and runs Compose during startup. Inside, I call builder.Services.Decorate<IUmbracoDatabaseFactory>(...). Decorate keeps the original registration intact, but anyone who asks the container for IUmbracoDatabaseFactory now gets my wrapper, with the real Umbraco factory tucked inside it as inner.

This matters because I'm not replacing Umbraco's factory. All of the connection string handling, provider resolution, SQL context, bulk insert provider, upgrade configuration, that all keeps coming from Umbraco. I only intercept the one method I care about.

2. The decorator

TimeoutSettingDatabaseFactory is a plain pass-through. Every property and method just forwards to _inner, except CreateDatabase():

public IUmbracoDatabase CreateDatabase()
{
    var db = _inner.CreateDatabase();
    db.CommandTimeout = _commandTimeout;
    return db;
}

Every time Umbraco asks its factory for a database, I let it build one, then set CommandTimeout before handing it back. Because CommandTimeout lives on the database (and propagates to the commands it creates), this is enough to widen the timeout for everything that goes through the Umbraco data layer.

1800 seconds - 30 minutes - is deliberately generous. It's an upper bound, not a target. Individual queries complete in milliseconds; the ceiling is there so that the one heavy operation I actually needed to run doesn't get killed mid-flight.

Why a composer and not appsettings.json?

My first instinct was to just set Command Timeout=... in the SQL Server connection string and be done with it. That didn't work, whatever value I put on the connection string was ignored by the time queries actually ran, because NPoco sets CommandTimeout on the database explicitly and that wins over the connection string setting.

Umbraco also doesn't expose a first-class CommandTimeout knob on IUmbracoDatabase that can be flipped in appsettings.json. So the composer route was the one that actually stuck: set it in code, on the database, after it's been created, exactly the layer that was overriding me in the first place.

Doing it in a composer also keeps the change visible in source control and reviewable in a PR, which is what I want for something that affects every query in the application.

Why decorate instead of replace?

I could have implemented IUmbracoDatabaseFactory from scratch and registered it myself. I didn't, because:

  • Umbraco's factory handles cold start, upgrade mode, connection string changes at runtime, and the ISqlContext / IBulkSqlInsertProvider wiring. Rewriting any of that is a liability.
  • Decorate composes cleanly with future Umbraco changes. If Umbraco ships a new implementation of IUmbracoDatabaseFactory in a later version, my decorator still wraps it without modification.
  • It keeps the diff tiny and the intent obvious: "take the real factory, bump the timeout on every database it creates."

Gotchas

A few things to be aware of if you copy this pattern:

  • It only affects IUmbracoDatabase. Direct SqlConnection/SqlCommand usage (e.g. raw ADO.NET in a custom service) is unaffected, I'd need to set CommandTimeout there myself, or decorate whatever abstraction owns them.
  • Raising a timeout doesn't make a query fast. 30 minutes of blocked connection can starve the pool. I use this for operations I've deliberately scoped, maintenance jobs, one-off migrations, heavy reporting queries, and fix the underlying query when it's hot-path code.
  • Scrutor's Decorate requires the service to already be registered when the composer runs. Because Umbraco registers IUmbracoDatabaseFactory before user composers execute, this works out of the box. If it ever doesn't, check composer ordering with [ComposeAfter].

Result

One small composer, one decorator, no changes at any call site. The long-running SQL that was timing out now has enough breathing room to finish, and everything else continues to behave exactly as Umbraco intended.

If you hit the same wall, reach for a decorator before you reach for per-query overrides, it's easier to reason about and much easier to remove later.

About the author

Aaron Sadler

Aaron Sadler, Umbraco MVP (3x), Umbraco Certified Master Developer and DevOps Engineer

comments powered by Disqus