dotnet/aspnetcore · critical · Error

Received persisted state for circuit ID

Error message

Received persisted state for circuit ID '${circuitId}', but the current circuit ID is '${this._circuitId}'.

What it means

Inside `JS.SavePersistedState` the handler compares the incoming `circuitId` against `this._circuitId`. A mismatch means the server is sending state for a circuit this client is not (or no longer) hosting — accepting it would corrupt the local persisted-state slot, so the handler throws with both ids for diagnostics.

Solutions

  1. Ensure sticky sessions are configured for Blazor Server in a load-balanced deployment so a client always reaches the server holding its circuit.
  2. Confirm client and server are the same build version so circuit id semantics match across resume.
  3. Avoid manually invoking resume/reconnect concurrently from multiple code paths.
  4. If the error recurs, capture the reported pair of ids and correlate with server circuit lifecycle logs to find the stale push.
Defensive patterns

Strategy: validation

Validate before calling

// guard the handler
if (circuitId !== this._circuitId) { this._logger.log(LogLevel.Error, `Stale state for ${circuitId}, have ${this._circuitId}`); return; }

Try / catch

try { /* save persisted state */ }
catch (e) { if (/current circuit ID/.test(e.message)) { /* mismatch - drop stale state */ return; } throw e; }

Prevention

When it happens

Trigger: Server transmits `JS.SavePersistedState` with a circuit id different from the one returned by `StartCircuit`/`ResumeCircuit` and stored in `_circuitId`. Typically a stale server-side circuit record pushing state, or a reconnect where a new circuit id was assigned but an old state push is still in flight.

Common situations: Reconnect/resume flows where the server created a fresh circuit but a queued persisted-state message references the previous one; load-balancer/sticky-session misconfiguration routing a client's messages to a server holding a different circuit; multiple tabs sharing a connection unexpectedly; server restart losing circuit identity.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/6ec95580488c9718. Report an issue: GitHub.

Appendix: source

Thrown at src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts:169

        this.changeActivity(1);
      }
      (this._dispatcher.beginInvokeJSFromDotNet as (...a: unknown[]) => unknown)
        .call(this._dispatcher, asyncHandle, ...rest);
    });
    connection.on('JS.EndInvokeDotNet', (asyncCallId: string, success: boolean, resultJsonOrExceptionMessage: string) => {
      if (asyncCallId) {
        this.changeActivity(-1);
      }
      this._dispatcher.endInvokeDotNetFromJS(asyncCallId, success, resultJsonOrExceptionMessage);
    });
    connection.on('JS.ReceiveByteArray', this._dispatcher.receiveByteArray.bind(this._dispatcher));

    connection.on('JS.SavePersistedState', (circuitId: string, components: string, applicationState: string) => {
      if (!this._circuitId) {
        throw new Error('Circuit host not initialized.');
      }
      if (circuitId !== this._circuitId) {
        throw new Error(`Received persisted state for circuit ID '${circuitId}', but the current circuit ID is '${this._circuitId}'.`);
      }
      this._persistedCircuitState = { components, applicationState };
      return true;
    });

    connection.on('JS.BeginTransmitStream', (streamId: number) => {
      const readableStream = new ReadableStream({
        start: (controller) => {
          this.changeActivity(1);
          connection.stream('SendDotNetStreamToJS', streamId).subscribe({
            next: (chunk: Uint8Array) => controller.enqueue(chunk),
            complete: () => { controller.close(); this.changeActivity(-1); },
            error: (err) => { controller.error(err); this.changeActivity(-1); },
          });
        },
      });

      this._dispatcher.supplyDotNetStream(streamId, readableStream);

View on GitHub (pinned to 3600ca084e)