pg-embed User Handbook

February 24, 2026 · View on GitHub

Practical guide to embedding a PostgreSQL server in your Rust application or test suite.


Installation

Add to Cargo.toml:

[dependencies]
pg-embed = "1.0"

The default feature set (rt_tokio_migrate) includes tokio, reqwest, and sqlx. For a smaller build without sqlx/migrations:

[dependencies]
pg-embed = { version = "1.0", default-features = false, features = ["rt_tokio"] }

At least one feature must be enabled.


Quick start

use std::time::Duration;
use pg_embed::pg_enums::PgAuthMethod;
use pg_embed::pg_errors::Result;
use pg_embed::pg_fetch::{PgFetchSettings, PG_V17};
use pg_embed::postgres::{PgEmbed, PgSettings};

#[tokio::main]
async fn main() -> Result<()> {
    let pg_settings = PgSettings {
        database_dir:  std::path::PathBuf::from("data/db"),
        port:          5432,
        user:          "postgres".to_string(),
        password:      "password".to_string(),
        auth_method:   PgAuthMethod::MD5,
        persistent:    false,
        timeout:       Some(Duration::from_secs(30)),
        migration_dir: None,
    };

    let fetch_settings = PgFetchSettings {
        version: PG_V17,
        ..Default::default()
    };

    let mut pg = PgEmbed::new(pg_settings, fetch_settings).await?;
    pg.setup().await?;   // download + unpack + initdb (cached after first run)
    pg.start_db().await?;

    // connection string: "postgres://postgres:password@localhost:5432/postgres"
    println!("{}", pg.full_db_uri("postgres"));

    pg.stop_db().await?;
    Ok(())
}

setup() downloads the binary package on first use and caches it. Subsequent runs skip the download.


PgSettings reference

FieldTypeRequiredDescription
database_dirPathBufyesDirectory for the PostgreSQL cluster data files. Created automatically.
portu16yesTCP port the server listens on.
userStringyesSuperuser username (passed to initdb).
passwordStringyesSuperuser password (written to a temp file, passed to initdb).
auth_methodPgAuthMethodyesAuthentication method for pg_hba.conf.
persistentboolyesIf false, the cluster is deleted when PgEmbed is dropped.
timeoutOption<Duration>yesTimeout for initdb, pg_ctl start, and pg_ctl stop. None = no timeout.
migration_dirOption<PathBuf>yesDirectory of .sql migration files. None = no migrations.

PgFetchSettings reference

FieldTypeDefaultDescription
hostStringhttps://repo1.maven.orgMaven repository base URL. Override to use a local mirror.
operating_systemOperationSystemdetected at compile timeTarget OS.
architectureArchitecturedetected at compile timeTarget CPU architecture.
versionPostgresVersionPG_V17PostgreSQL version to download. Prefer an explicit constant.

Available version constants: PG_V10, PG_V11, PG_V12, PG_V13, PG_V14, PG_V15, PG_V16, PG_V17, PG_V18.


Authentication methods

PgAuthMethodpg_hba.conf valueNotes
PlainpasswordPlaintext — for development only
MD5md5MD5-hashed password
ScramSha256scram-sha-256Recommended for PostgreSQL ≥ 11

Platform support

Binaries are provided by zonkyio/embedded-postgres-binaries.

OSArchitectures supported
macOSamd64, arm64v8
Linux (glibc)amd64, i386, arm32v6, arm32v7, arm64v8, ppc64le
Alpine Linuxamd64, i386, arm32v6, arm32v7, arm64v8, ppc64le
Windowsamd64, i386

The OperationSystem and Architecture defaults are detected at compile time. Override via PgFetchSettings to cross-target.


Connection strings

After start_db(), use:

// Base URI: postgres://{user}:{password}@localhost:{port}/{db}
let uri = pg.full_db_uri("mydb");
// e.g. "postgres://postgres:password@localhost:5432/mydb"

For the default postgres database:

let uri = pg.full_db_uri("postgres");

Database operations (rt_tokio_migrate only)

These methods require the rt_tokio_migrate feature (the default).

pg.create_database("mydb").await?;
assert!(pg.database_exists("mydb").await?);
pg.drop_database("mydb").await?;

Installing extensions

Third-party extensions (pgvector, PostGIS, etc.) are not included in the precompiled binaries. To use one, obtain the pre-built files for your platform and call install_extension() after setup() and before start_db().

use std::path::Path;

pg.setup().await?;
pg.install_extension(Path::new("extensions/pgvector")).await?;
pg.start_db().await?;

// Then inside a database:
// CREATE EXTENSION IF NOT EXISTS vector;

install_extension() copies files from the given directory into the binary cache:

File extensionDestination
.so, .dylib, .dll{cache}/lib/
.control, .sql{cache}/share/postgresql/extension/
Anything elseSkipped

Pure-SQL extensions (no shared library) work identically — simply omit the binary file.


Running migrations

Place numbered .sql files in a directory:

migrations/
  01_create_users.sql
  02_add_email.sql

Configure PgSettings:

PgSettings {
    migration_dir: Some(PathBuf::from("migrations")),
    // …
}

Then after starting and creating the database:

pg.create_database("mydb").await?;
pg.migrate("mydb").await?;

Migrations are applied in filename order using the sqlx migrator.


Persistent vs. ephemeral clusters

persistentBehaviour on Drop
falsestop_db_sync() is called, then the database dir and password file are deleted.
truestop_db_sync() is called, data files are left on disk.

For persistent clusters, call PgAccess::clean_up(database_dir, pw_file_path) to clean up manually.


Binary cache

Downloaded binaries are cached at:

OSCache location
macOS~/Library/Caches/pg-embed/{os}/{arch}/{version}/
Linux~/.cache/pg-embed/{os}/{arch}/{version}/
Windows%LOCALAPPDATA%\pg-embed\{os}\{arch}\{version}\

To clear the cache from code:

PgAccess::purge().await?;

Multiple simultaneous instances

Run multiple servers on different ports:

let mut pg1 = PgEmbed::new(settings_on_5432, fetch_settings.clone()).await?;
let mut pg2 = PgEmbed::new(settings_on_5433, fetch_settings.clone()).await?;

pg1.setup().await?;
pg2.setup().await?;

pg1.start_db().await?;
pg2.start_db().await?;

A global lock (ACQUIRED_PG_BINS) ensures the binary package is only downloaded once even if both instances initialise concurrently.


Using a local Maven mirror

Override host to point at a local artifact proxy:

let fetch_settings = PgFetchSettings {
    host: "https://my-artifactory.internal".to_string(),
    version: PG_V17,
    ..Default::default()
};

Logging

pg-embed uses the log crate. Enable output with any compatible backend:

env_logger::Builder::from_env(
    env_logger::Env::default().default_filter_or("info")
).init();

For detailed output including initdb / pg_ctl stdout lines, use RUST_LOG=debug.


FAQ

Q: The first test run is slow. A: pg-embed downloads the binary package (~20–60 MB depending on OS/arch) on first use and caches it. Subsequent runs are fast.

Q: Tests fail with "port already in use". A: Use serial_test to prevent concurrent tests from binding the same port:

use serial_test::file_serial;

#[tokio::test]
#[file_serial(pg_port_5432)]
async fn my_test() { … }

Use #[file_serial] (not #[serial]) if you run multiple test binaries, as file locks work across processes.

Q: The data directory is not cleaned up after tests. A: Use persistent: false and ensure the PgEmbed value is dropped before the test ends. If you store the database dir in a tempfile::TempDir, declare the TempDir before PgEmbed so that PgEmbed drops first.

Q: How do I choose a timeout? A: initdb typically takes 1–5 seconds on a warm machine; pg_ctl start is similar. 10–30 seconds is safe for most CI environments. Set timeout: None to disable the timeout entirely.

Q: ScramSha256 fails on PostgreSQL 10. A: SCRAM-SHA-256 was introduced in PostgreSQL 10 but some client libraries only support it from PG 11. Use PgAuthMethod::MD5 for maximum compatibility.

Q: Can I use pg-embed without internet access? A: Yes. Set host to a local mirror URL, or pre-populate the cache directory at the path described above with the appropriate .jar / unpacked binaries.