Skip to content

Web: dedicated sqlite3_worker is never terminated, leaks one per Flutter Web hot restart #427

@khawarizmus

Description

@khawarizmus

Summary

On Flutter Web, every time a PowerSyncDatabase is opened, sqlite3_web spawns a dedicated web worker named sqlite3_worker that is never terminate()d. close()/dispose() only sends a CloseDatabase message — it does not tear the worker down. Because a hot restart re-runs main() in the same JS VM (Dart state is wiped, but the page and its workers persist), each hot restart spawns a fresh sqlite3_worker and orphans the previous one. They accumulate until a full browser refresh recreates the JS VM.

Impact is development-only (duplicated logs, harder debugging after hot restart); production is unaffected since the VM isn't repeatedly re-initialised. Reported on powersync: 2.2.0.

Context: https://discord.com/channels/1138230179878154300/1511710494766141540

Environment

  • powersync 2.2.0 (pulls sqlite3_web 0.8.1, sqlite_async 0.14.2)
  • Flutter Web, debug, hot restart (not hot reload — hot reload doesn't re-run main())
  • Reproduced on plain desktop Chromium (Chrome for Testing 127), no special headers

Steps to reproduce

  1. Minimal Flutter Web app on powersync 2.2.0. In main(), open a PowerSyncDatabase and initialize() it. No backend, no connect(), no schema specifics required — the worker is spawned at open.
  2. Run on web (flutter run -d chrome).
  3. Press R (hot restart) a few times.
  4. Count dedicated workers named sqlite3_worker (Chrome Task Manager, or programmatically).

Expected vs actual

  • Expected: hot restart leaves a single live sqlite3_worker; the previous VM run's worker is torn down.
  • Actual: the count of sqlite3_worker instances grows by one per hot restart and is only cleared by a full browser refresh.

Root cause

sqlite3_web 0.8.1's DatabaseClient.startWorkers() (lib/src/client.dart:429) runs both _startDedicated() and _startShared() during feature detection — neither is gated on which access mode ultimately hosts the DB. _startDedicated() spawns, via _DefaultWorkerConnector.spawnDedicatedWorker():

// sqlite3_web/lib/src/worker_connector.dart:183
Worker(_workerUrl.toJS, WorkerOptions(name: 'sqlite3_worker'))

and nothing in sqlite3_web ever calls .terminate() (zero terminate references in the package). PowerSync's web teardown (close() / sync controller close()) sends CloseDatabase over the channel but never retains or terminates the dedicated Worker handle. So:

  • The dedicated sqlite3_worker is spawned regardless of which access mode hosts the DB — even when a shared worker is selected for the actual database — and is never torn down.
  • Within a single JS-VM / Dart program state the worker is created once and shared across opens: sqlite_async caches the WebSqlite instance in a library-global map keyed by wasm+worker URL (sqlite_async/lib/src/web/web_sqlite_open_factory.dart:14-32). (This is why opening N databases in one VM does not spawn N workers.)
  • A Flutter-web hot restart re-runs main(), resetting that Dart-side cache, so the next open rebuilds WebSqlite and spawns a fresh sqlite3_worker while the prior one is orphaned in the surviving page — one extra worker per restart, cleared only by a full browser refresh (new JS VM).

This is the web analogue of the native fix in powersync 2.2.0 ("Fix disconnect() call leaving stale database isolates behind") — the same teardown gap, in the web worker execution context.

AI Disclosure

Used Claude Code with Opus 4.7 to research, debug the issue, and assist in writing a test for reproducing it. Verified with the team internally first.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions