Avoid Using OnStatusChanged in Optimizely CMS – It Can Impact Database Performance

Posted on Sunday, November 9, 2025

Beware of Overusing OnStatusChanged in Optimizely CMS Scheduled Jobs

Optimizely CMS allows you to create scheduled jobs — a powerful feature often used to automate repetitive tasks such as product imports. Sometimes, we even use them for one-off data migrations and delete them afterward.

This post assumes you’re already familiar with the basics, but if not, you can catch up here: Optimizely Scheduled Jobs Documentation

Most developers are aware of the OnStatusChanged method. It’s handy for updating the administrative interface with progress messages while a scheduled job runs. For example, you might use it to display periodic updates like “Importing products…” or “Processing batch 2 of 5…” so that anyone monitoring the job can see what’s happening in real time.

There’s a small hiccup, though.


The Problem

While investigating a performance issue for one of our clients, we noticed the database was under heavy load during certain scheduled jobs. After some digging, we realized that our frequent use of OnStatusChanged was the culprit.

Even worse, some jobs began failing with an unexpected error — one that had nothing to do with the job logic itself:

An unhandled error occured while running the job 'MyJob'.
Microsoft.Data.SqlClient.SqlException (0x80131904): The server failed to resume the transaction. Desc:3e00000334.
The transaction active in this session has been committed or aborted by another session.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.TdsParser.TdsExecuteTransactionManagerRequest(Byte[] buffer, TransactionManagerRequestType request, String transactionName, TransactionManagerIsolationLevel isoLevel, Int32 timeout, SqlInternalTransaction transaction, TdsParserStateObject stateObj, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction2005(TransactionRequest transactionRequest, String transactionName, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction(TransactionRequest transactionRequest, String name, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnection.BeginSqlTransaction(IsolationLevel iso, String transactionName, Boolean shouldReconnect)
   at Microsoft.Data.SqlClient.SqlConnection.BeginTransaction(IsolationLevel iso, String transactionName)
   at Microsoft.Data.SqlClient.SqlConnection.BeginTransaction(IsolationLevel iso)
   at Microsoft.Data.SqlClient.SqlConnection.BeginDbTransaction(IsolationLevel isolationLevel)
   at EPiServer.Data.Providers.Internal.ConnectionContext.BeginTransaction()
   at EPiServer.Data.Internal.DefaultConnectionContextHandler.CreateConnectionScope(Boolean requireTransaction, Action completeAction)
   at EPiServer.Data.Internal.ConnectionScopeResolver.GetConnectionScope(Boolean requireTransaction)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.GetConnection(Boolean requireTransaction)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.<>c__DisplayClass28_0`1.<ExecuteTransaction>b__0()
   at EPiServer.Data.Providers.SqlTransientErrorsRetryPolicy.Execute[TResult](Func`1 method)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.ExecuteTransaction[TResult](Func`1 action)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.ExecuteTransaction(Action action)
   at EPiServer.DataAccess.Internal.SchedulerDB.UpdateCurrentStatusMessage(Guid id, String statusMessage)
   at EPiServer.Scheduler.Internal.DefaultScheduledJobExecutor.JobInstance_StatusChanged(Object sender, JobStatusChangedEventArgs e)
   at EPiServer.Scheduler.ScheduledJobBase.OnStatusChanged(String statusMessage)

What’s Going On

Digging through the stack trace reveals that each call to OnStatusChanged triggers a database write. Every single status update results in a new SQL transaction. If your job updates its status too frequently — say, inside a loop — you can easily overwhelm the database with hundreds or thousands of writes.

In our case, this not only degraded performance but also caused transaction errors like the one above.


The Fix

Once we understood what was happening, we simply removed (or drastically reduced) our calls to OnStatusChanged. That immediately stabilized the system and reduced database load.

If your scheduled jobs make frequent calls to OnStatusChanged, we strongly suggest you review and limit them. Consider logging detailed progress elsewhere (e.g., a file, Application Insights, or custom monitoring) and only update the UI when it truly matters — such as at the start, at major milestones, or upon completion.


Takeaway

OnStatusChanged is a useful feature, but it’s not free. Every update hits the database. Use it sparingly to keep your jobs reliable and performant.

Hope this helps someone avoid the same headache we had!