Summary
We're hitting SqliteException: SQLite Error 21: 'powersync_control: invalid state: No iteration is active' from inside StreamingSyncImplementation.RustStreamingSyncIteration in a MAUI iOS app. In our case iOS suspended the app for about 15 minutes (which kills the socket), and when the app resumed and reconnected, control invocations that were still queued for the old iteration got forwarded to the Rust core after that iteration had already ended. The core rejects them with the error above.
This looks like the .NET version of powersync-js #943 ("Queued control commands sent after CloseSyncStream"). That one was fixed in the JS core in #929 and released in the Node (v0.19.2), React Native (v1.35.7) and Web (v1.39.0) SDKs, but I don't see an equivalent guard in the dotnet SDK.
Not the same bug as #75 (the updateBuffer thread safety issue), though it surfaces the same way: an unobserved task exception escaping the sync iteration task.
Versions
PowerSync.Common / PowerSync.Maui 0.1.3 (latest on NuGet as of 2026-07-16). .NET MAUI, iOS, self-hosted PowerSync service.
Stack trace
From our crash reporting. We got two identical exceptions about 15ms apart, both as TaskExceptionHolder_UnhandledException:
SqliteException: SQLite Error 21: 'powersync_control: invalid state: No iteration is active'.
at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32, sqlite3)
at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior)
at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReaderAsync(CommandBehavior, CancellationToken)
at Microsoft.Data.Sqlite.SqliteCommand.ExecuteDbDataReaderAsync(CommandBehavior, CancellationToken)
at Dapper.SqlMapper.QueryRowAsync<SqliteBucketStorage.ControlResult>(...)
at PowerSync.Common.Client.Sync.Bucket.SqliteBucketStorage.<>c__DisplayClass28_0.<<Control>b__0>d.MoveNext()
at PowerSync.Common.MDSQLite.MDSQLiteAdapter.<>c__DisplayClass36_0`1.<<InternalTransaction>b__0>d[String].MoveNext()
at PowerSync.Common.MDSQLite.MDSQLiteAdapter.RunTransaction(MDSQLiteTransaction ctx, Func`1 action)
at PowerSync.Common.MDSQLite.MDSQLiteAdapter.InternalTransaction<String>(...)
at PowerSync.Common.MDSQLite.MDSQLiteAdapter.WriteLock<String>(...)
at PowerSync.Common.MDSQLite.MDSQLiteAdapter.WriteTransaction<String>(...)
at PowerSync.Common.Client.Sync.Bucket.SqliteBucketStorage.Control(String op, Object payload)
at PowerSync.Common.Client.Sync.Stream.StreamingSyncImplementation.<>c__DisplayClass41_0.<<RustStreamingSyncIteration>g__Control|3>d.MoveNext()
at PowerSync.Common.Client.Sync.Stream.StreamingSyncImplementation.<>c__DisplayClass41_0.<<RustStreamingSyncIteration>b__6>d.MoveNext()
Root cause
Once the core iteration has been stopped, control invocations that were queued while it was live still get drained and sent through powersync_control. There is no active iteration at that point, so the core returns the invalid state error, and since this happens inside the iteration task it comes through as an unobserved exception. Nothing the app can catch.
Reproducing
It's timing dependent, so it doesn't happen on every reconnect. What triggered it for us:
- Streaming sync connected and healthy on iOS.
- App gets backgrounded and iOS suspends it for a few minutes, killing the socket.
- App comes back to the foreground and our resume handler calls
PowerSyncDatabase.Connect(...) again.
- The old iteration tears down while control ops are still queued, and two
SqliteExceptions escape as unobserved task exceptions.
Frequent reconnects make it more likely. We were running short-lived sync JWTs at the time, so the client was re-establishing the stream on every token expiry, and every one of those teardowns is a chance to hit this.
Impact
Nothing crashes and no data is lost; sync recovers on the next iteration. But the app has no way to catch or suppress these, so they show up as unhandled-exception noise in crash telemetry for every session that hits the race.
Suggested fix
Port the fix from powersync-js #929: stop draining queued control invocations once the iteration is closed. Alternatively (or additionally), treat this error as an expected outcome of an aborted iteration instead of letting it escape the task.
Files
PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs: RustStreamingSyncIteration and its local Control function
PowerSync/PowerSync.Common/Client/Sync/Bucket/SqliteBucketStorage.cs: Control
Summary
We're hitting
SqliteException: SQLite Error 21: 'powersync_control: invalid state: No iteration is active'from insideStreamingSyncImplementation.RustStreamingSyncIterationin a MAUI iOS app. In our case iOS suspended the app for about 15 minutes (which kills the socket), and when the app resumed and reconnected, control invocations that were still queued for the old iteration got forwarded to the Rust core after that iteration had already ended. The core rejects them with the error above.This looks like the .NET version of powersync-js #943 ("Queued control commands sent after
CloseSyncStream"). That one was fixed in the JS core in #929 and released in the Node (v0.19.2), React Native (v1.35.7) and Web (v1.39.0) SDKs, but I don't see an equivalent guard in the dotnet SDK.Not the same bug as #75 (the
updateBufferthread safety issue), though it surfaces the same way: an unobserved task exception escaping the sync iteration task.Versions
PowerSync.Common/PowerSync.Maui0.1.3 (latest on NuGet as of 2026-07-16). .NET MAUI, iOS, self-hosted PowerSync service.Stack trace
From our crash reporting. We got two identical exceptions about 15ms apart, both as
TaskExceptionHolder_UnhandledException:Root cause
Once the core iteration has been stopped, control invocations that were queued while it was live still get drained and sent through
powersync_control. There is no active iteration at that point, so the core returns the invalid state error, and since this happens inside the iteration task it comes through as an unobserved exception. Nothing the app can catch.Reproducing
It's timing dependent, so it doesn't happen on every reconnect. What triggered it for us:
PowerSyncDatabase.Connect(...)again.SqliteExceptions escape as unobserved task exceptions.Frequent reconnects make it more likely. We were running short-lived sync JWTs at the time, so the client was re-establishing the stream on every token expiry, and every one of those teardowns is a chance to hit this.
Impact
Nothing crashes and no data is lost; sync recovers on the next iteration. But the app has no way to catch or suppress these, so they show up as unhandled-exception noise in crash telemetry for every session that hits the race.
Suggested fix
Port the fix from powersync-js #929: stop draining queued control invocations once the iteration is closed. Alternatively (or additionally), treat this error as an expected outcome of an aborted iteration instead of letting it escape the task.
Files
PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs:RustStreamingSyncIterationand its localControlfunctionPowerSync/PowerSync.Common/Client/Sync/Bucket/SqliteBucketStorage.cs:Control