Two Approaches to "Embedding" PostgreSQL: postgresql-embedded vs PGlite
TL;DR
For embedding PostgreSQL into an application, there is postgresql-embedded for Rust and PGlite for JS/TS. The former downloads and starts a native PostgreSQL binary and is fully PostgreSQL-compatible. The latter is a lightweight WASM-compiled version (3MB gzipped) that even runs in browsers and at the edge. The main differences are the target language, the runtime environment, and whether multiple connections are supported. Which one you pick is determined by your application’s runtime.
Why “Embedded PostgreSQL” Is Needed
You want to use PostgreSQL in tests or development, but spinning up a Docker container is overkill. Or you want to execute SQL inside a browser. The need for “real PostgreSQL, but without managing an external process” is surprisingly common.
SQLite covers single-file scenarios, but if you depend on PostgreSQL-specific features (jsonb, pgvector, the subtle behavior of window functions, etc.), SQLite is not a substitute.
Two projects tackle this problem with completely different approaches.
postgresql-embedded: Auto-Manage a Native Binary
postgresql-embedded is a Rust crate. What it does is simple: download the native PostgreSQL binary, start it in a temporary directory, and clean up when done.
use postgresql_embedded::PostgreSQL;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut postgresql = PostgreSQL::default();
postgresql.setup().await?; // download & extract the binary
postgresql.start().await?; // start the PostgreSQL process
let database_url = postgresql.settings().url("test");
// From here on, use it like any normal PostgreSQL
postgresql.stop().await?; // stop the process and clean up
Ok(())
}
Once started, it is just regular PostgreSQL, so you can connect via psql, and multi-connection is naturally supported. C extensions also work as is.
The tradeoffs, however, are clear.
- The binary is downloaded on first start (several hundred MB)
- It’s a native OS process, so it doesn’t run in browsers or edge environments
- Rust ecosystem only (it’s not impossible via FFI from other languages, but not practical)
Use cases center on integration tests for Rust applications and starting a DB on CI.
PGlite: Run PostgreSQL In-Process via WASM
PGlite is a project from ElectricSQL that compiles PostgreSQL to WASM via Emscripten and runs it in-process inside a JavaScript runtime.
import { PGlite } from "@electric-sql/pglite";
const db = new PGlite(); // In-memory DB (default)
// const db = new PGlite("idb://my-db"); // Browser: persist to IndexedDB
// const db = new PGlite("./path/to/data"); // Node.js: persist to filesystem
await db.exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
`);
const result = await db.query("SELECT * FROM users;");
console.log(result.rows);
The biggest feature is its lightness — about 3MB gzipped. Because it can persist to the browser’s IndexedDB, you can use PostgreSQL syntax and features in offline-first applications as a drop-in replacement for SQLite.
The pgvector extension is also supported, so you can run vector search inside the browser.
There are structural constraints, however.
- Single-process, single-connection: Concurrent connections from multiple clients are not assumed
- C extensions don’t run: Only extensions compiled to WASM are usable
- Performance: Going through the WASM execution layer is slower than native
Fundamental Architectural Difference
In one sentence: the two differ in what part of PostgreSQL they embed.
flowchart LR
subgraph pe["postgresql-embedded"]
A["Rust app"] -->|"Spawn process"| B["PostgreSQL\nnative binary"]
B -->|"TCP/Unix Socket"| A
end
subgraph pg["PGlite"]
C["JS/TS app"] -->|"Function call"| D["PostgreSQL\nWASM module"]
D -->|"Direct return"| C
end
postgresql-embedded provides “lifecycle management of a PostgreSQL process”. The app and DB are separate processes communicating over TCP/Unix socket. In other words, it is an automation tool for an external process.
PGlite embeds “the PostgreSQL engine itself” inside the app as WASM. There is no inter-process communication; queries are executed via function calls. In other words, it is an in-process database engine.
Comparison Table
| Aspect | postgresql-embedded | PGlite |
|---|---|---|
| Language | Rust | TypeScript / JavaScript |
| Embedding model | Native binary launch | WASM in-process execution |
| Binary size | Hundreds of MB (full PostgreSQL) | ~3MB (gzipped) |
| Multi-connection | ✅ | ❌ (single user) |
| Runs in browser | ❌ | ✅ |
| Node.js / Deno / Bun | ❌ | ✅ |
| C extensions | ✅ | ❌ |
| pgvector | ✅ | ✅ |
| Persistence | Filesystem (standard PGDATA) | IndexedDB / filesystem / memory |
| PostgreSQL compatibility | Full | Nearly full (some limits) |
Which to Choose
The criterion is actually simple: it’s determined by the application’s runtime.
- Writing in Rust → postgresql-embedded
- Writing in JS/TS → PGlite
- Want it in a browser → PGlite, no contest
- Need multi-connection → postgresql-embedded
- Integration tests in CI → either is possible; match the language
The two are not really competitors — their target ecosystems are different. You can’t “embed PGlite in a Rust app” or “use postgresql-embedded in a browser”, so in practice you rarely face a hard choice.
What’s worth noting is that, for the same need — “embed PostgreSQL in an app instead of running it as an external service” — different approaches are emerging per language ecosystem. SQLite’s position as the embedded DB is unshaken, but it’s a good trend that, when PostgreSQL-specific features are required, “well, just use Docker” is no longer the only option.
That’s all.