microsoft/semantic-kernel · error · InvalidOperationException

Runtime is already stopping.

Error message

Runtime is already stopping.

What it means

InProcessRuntime.StopAsync throws InvalidOperationException when _finishSource is already non-null, meaning a stop is already in progress. StopAsync is idempotent only before the first stop; once _finishSource is created it refuses concurrent/duplicate stop calls.

Source

Thrown at dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs:77

        this._shutdownSource = new CancellationTokenSource();
        this._messageDeliveryTask = Task.Run(() => this.RunAsync(this._shutdownSource.Token), cancellationToken);

        return Task.CompletedTask;
    }

    /// <summary>
    /// Stops the runtime service.
    /// </summary>
    /// <param name="cancellationToken">Token to propagate when stopping the runtime.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the runtime is in the process of stopping.</exception>
    public Task StopAsync(CancellationToken cancellationToken = default)
    {
        if (this._shutdownSource != null)
        {
            if (this._finishSource != null)
            {
                throw new InvalidOperationException("Runtime is already stopping.");
            }

            this._finishSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

            this._shutdownSource.Cancel();
        }

        return Task.CompletedTask;
    }

    /// <summary>
    /// This will run until the message queue is empty and then stop the runtime.
    /// </summary>
    public async Task RunUntilIdleAsync()
    {
        Func<bool> oldShouldContinue = this._shouldContinue;
        this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty;

View on GitHub (pinned to c028a0c7dc)

Solutions

  1. Call StopAsync once and await the resulting drain; do not stop concurrently.
  2. Coordinate shutdown through a single path (e.g. one CancellationTokenSource).
  3. Prefer DisposeAsync for teardown; it runs until idle and disposes the sources.

Example fix

// before
await Task.WhenAll(runtime.StopAsync(), runtime.StopAsync()); // second throws

// after
await runtime.StopAsync();
await runtime.RunUntilIdleAsync();
Defensive patterns

Strategy: validation

Validate before calling

bool stopping = false;
void SafeStop(InProcessRuntime rt)
{
    if (stopping) return;
    try { rt.StopAsync(); stopping = true; }
    catch (InvalidOperationException) { /* already stopping */ }
}

Try / catch

try { runtime.StopAsync(); }
catch (InvalidOperationException) { /* already stopping */ }

Prevention

When it happens

Trigger: Calling StopAsync twice in quick succession; concurrent shutdown from multiple threads/tasks; calling StopAsync while RunUntilIdleAsync is still draining.

Common situations: Race between an explicit StopAsync and DisposeAsync (which calls RunUntilIdleAsync then disposes sources); multiple cancellation signals firing shutdown; finally blocks on concurrent paths.

Related errors


AI-assisted analysis of microsoft/semantic-kernel@c028a0c7dc (2026-08-13). Data as JSON: /api/errors/450d7a2c82f04f94. Report an issue: GitHub.