Standalone dev/embedded-db harness (boots embedded pglite Postgres, execs windmill against it; plus a schema-compat probe) and a write-up of the experiment. The enabling migration-bootstrap fix landed separately in #9970 (now on main). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.6 KiB
Experiment: oliphaunt / pglite-oxide as a lightweight dev-mode database (WIN-2130)
Goal: find out whether Windmill can run against
oliphaunt — published on crates.io as
pglite-oxide — as a "lightweight dev
mode": an embedded Postgres with no Docker and no separate Postgres install, so
that cargo run alone gives you a working instance. The bar was "ideally get
scripts and flows running".
Verdict (TL;DR): it works. With a small migration-bootstrap fix (merged to
main in #9970), a full Windmill server+worker boots on the embedded
pglite-oxide database (PostgreSQL 17.5 on wasm32-unknown-wasix) and runs
scripts and flows — over a single database connection.
SCRIPT: {"started":true,"success":true,"completed":true,"result":"6x7=42"}
FLOW: {"started":true,"success":true,"completed":true,"result":"step-b-ok-flow-complete"}
PostgreSQL version: PostgreSQL 17.5 on wasm32-unknown-wasix … 32-bit
The reproducible harness lives in dev/embedded-db/.
What oliphaunt / pglite-oxide is
- PostgreSQL 17.5 compiled to WASIX (PGlite lineage), run in-process through a
bundled
wasmerruntime. No Docker, no local Postgres, no runtime build. PgliteServer::builder().path(dir).start()boots a local server andserver.database_url()returns a normalpostgresql://…URL for SQLx /tokio-postgres/psql.- Bundles
pgvector,pg_trgm,hstore,citext,ltree,pg_dump. - Serves one client connection at a time — the crate is explicit: "the server owns one embedded backend, so downstream pools should use a single connection." This single-connection property is the whole story below.
The key insight: single-connection ≡ max_connections=1
pglite's "one backend at a time" behaves exactly like pointing Windmill at any
normal Postgres with a max_connections=1 SQLx pool: the first physical
connection is served; a concurrently-needed second one blocks. That equivalence
let the whole investigation run against the fast local dev Postgres (with
DATABASE_CONNECTIONS=1) and only use pglite for final confirmation.
The runtime does NOT need two connections
Running Windmill standalone against a normal Postgres with the runtime pool capped
at one connection (DATABASE_CONNECTIONS=1), the server and worker share that
single connection and scripts and flows execute fine — the instance holds
exactly one live Postgres connection the whole time (verified via
pg_stat_activity). Everything else people assume "needs" concurrency — HTTP
handlers, the worker job-poll loop, the health checker — just serializes on the
pool. That's slower under load, but perfectly correct for dev.
So most of what looks like a concurrency requirement is only pool contention
that queues. The real blockers are the few places that hold one connection open
and then await a second acquisition — a genuine self-deadlock at
max_connections=1. There are exactly three, and all three are in the one-time
migration bootstrap:
| # | Site | Problem |
|---|---|---|
| 1 | migrate() housekeeping (windmill-api/src/db.rs) |
Holds db.acquire() (conn #1) while running DELETE … _sqlx_migrations via .execute(db) (needs conn #2) |
| 2 | migrate() stale-migration cleanup (same file) |
Same pattern inside the per-migration loop |
| 3 | fix_flow_versioning_migration (windmill-api/src/live_migrations.rs) |
Holds the migrator connection (with the migration advisory lock) while doing fetch_one(db) / db.begin() on the pool |
initial_connection() also hardcodes max_connections(2), but that's only a
ceiling — once the three sites above stop asking for a second connection, the
migration phase only ever uses one, so the ceiling is never hit.
The fix (merged in #9970)
Route those three migration-phase queries onto the connection the migrator has
already checked out (and, for #3, already holds the advisory lock on) instead
of re-acquiring from the pool. A CustomMigrator::connection() accessor
(windmill-api/src/db.rs) exposes that held connection; migrate() and
fix_flow_versioning_migration (windmill-api/src/live_migrations.rs) use it.
This is not a pglite-specific hack — it is a strict improvement for any
connection-constrained Postgres (managed providers with tight max_connections,
PgBouncer transaction pooling, etc.): fewer connections during migration, and for
#3 the existence-check and the write now run on the same advisory-locked
connection (tighter, not looser). Default behavior is unchanged for normal
multi-connection setups.
With it in place, Windmill boots on pglite and runs scripts and flows (see the
TL;DR output). No SKIP_MIGRATION, no schema edits.
What else was needed (already handled by Windmill)
uuid-osspis the only extension pglite lacks, and Windmill doesn't need it:uuid_generate_v*is never used (the schema uses built-ingen_random_uuid()), and Windmill already strips thatCREATE EXTENSIONline inOVERRIDDEN_MIGRATIONS(a compat path for managed providers that forbidCREATE EXTENSION). All 1182 migrations otherwise apply cleanly — verified independently by theschema-compatprobe (~950 ms on a single connection).
Caveats / known limitations
- Serialized on one connection — correct but not fast under concurrent load. Fine for a single-developer dev instance; not for anything multi-user.
- 32-bit wasm backend (
wasm32-unknown-wasix) — 4 GiB address-space ceiling and lower memory limits than native Postgres. - Alpha dependency pin.
pglite-oxide 0.5.1pullswasmer/wasmer-wasix0.702.0-alpha.3; cargo resolves the transitivevirtual-netto stable0.702.0(which addedNetworkError::MessageSize), and the alphawasmer-wasixdoesn't handle it → won't compile. Pin withcargo update -p virtual-net --precise 0.702.0-alpha.3(the harness commits aCargo.lockwith this pin). - Language runtimes are orthogonal. Scripts/flows run to the extent their
language is compiled into the binary (e.g. a
--features quickjs-only build runs bash but not Python) — unrelated to the database.
Recommendation
- The migration-bootstrap fix is worth taking on its own merits — it removes an unnecessary second-connection requirement during migration that also affects tightly connection-limited managed Postgres, not just pglite.
- With it,
pglite-oxideis a viable zero-dependency dev database: one binary, no Docker, no Postgres install, boots in ~200 ms, runs scripts and flows. A natural next step is a first-class--embedded-dbdev flag that boots pglite and wiresDATABASE_URLautomatically (the harness indev/embedded-dbprototypes exactly this). - Keep the single-connection caveat in mind: it's a dev-mode convenience, not a path to running production or load tests embedded.
Reproducing
See dev/embedded-db/README.md. In short:
# Prove the schema migrates on a single connection:
cd dev/embedded-db && cargo run --bin schema-compat
# Boot a real Windmill (built with the migration fix) on embedded pglite:
cargo run -- ../../backend/target/debug/windmill
# then hit http://localhost:$PORT and run a script / flow
Isolate the blockers yourself without pglite, using the equivalence above:
# fresh empty DB + runtime pool capped at 1 connection
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5432/<empty_db> \
DATABASE_CONNECTIONS=1 PORT=8273 ./backend/target/debug/windmill
# scripts and flows run; pg_stat_activity shows a single connection