MysticMind.PostgresEmbed
5.2.0
See the version list below for details.
dotnet add package MysticMind.PostgresEmbed --version 5.2.0
NuGet\Install-Package MysticMind.PostgresEmbed -Version 5.2.0
<PackageReference Include="MysticMind.PostgresEmbed" Version="5.2.0" />
<PackageVersion Include="MysticMind.PostgresEmbed" Version="5.2.0" />
<PackageReference Include="MysticMind.PostgresEmbed" />
paket add MysticMind.PostgresEmbed --version 5.2.0
#r "nuget: MysticMind.PostgresEmbed, 5.2.0"
#:package MysticMind.PostgresEmbed@5.2.0
#addin nuget:?package=MysticMind.PostgresEmbed&version=5.2.0
#tool nuget:?package=MysticMind.PostgresEmbed&version=5.2.0
MysticMind.PostgresEmbed Postgres embedded database equivalent for .Net applications

Run a real PostgreSQL server, extensions included, from .NET code and tests on Windows, Linux (glibc and Alpine/musl) and macOS. No Docker and no local Postgres install required.
By default, this library uses the minimal binaries published by zonkyio/embedded-postgres-binaries. They are much smaller than the official downloads. A list of all available Postgres versions is here: https://mvnrepository.com/artifact/io.zonky.test.postgres/embedded-postgres-binaries-bom. Click a version to see the platforms it is published for.
The library detects the OS and CPU architecture and downloads the matching binaries.
If you have benefitted from this library and it has saved you a bunch of time, please feel free to sponsor my work!<br> <a href="https://github.com/sponsors/mysticmind" target="_blank"><img height="30" style="border:0px;height:36px;" src="https://img.shields.io/static/v1?label=GitHub Sponsor&message=%E2%9D%A4&logo=GitHub" border="0" alt="GitHub Sponsor" /></a>
Requirements
- .NET 8 or later (the package targets .NET 8, 9, 10 and 11)
- Windows x64, Linux x64/arm64 (glibc or musl), macOS x64/arm64. Windows on Arm runs the x64 build under emulation.
- Postgres refuses to run as root/Administrator, so run your tests as a regular user (this matters in Docker containers)
Usage
Install the package from NuGet:
dotnet add package MysticMind.PostgresEmbed
Start a server
await using var server = new PgServer("17.6.0");
await server.StartAsync();
// trust authentication is used, so no password is needed
await using var conn = new NpgsqlConnection(server.ConnectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand("CREATE TABLE table1(id int PRIMARY KEY, title text)", conn);
await cmd.ExecuteNonQueryAsync();
Disposing the server stops it. You can also call StopAsync() yourself.
Choosing a version
PgVersion takes an exact version or a spec that resolves to the newest matching version published for your platform:
new PgServer("17.6.0"); // exactly this version
new PgServer("17"); // newest 17.x
new PgServer("17.6"); // newest 17.6.x
new PgServer("latest"); // newest of all
A spec is resolved when the server starts (server.PgVersion then holds the exact version). Without network access, it falls back to the newest matching version already downloaded. To see what is published:
IReadOnlyList<string> versions = await PgVersions.GetAvailableAsync(); // for this platform, oldest first
Warming the cache, for example in CI
InstallAsync() downloads and extracts the binaries (and configured extensions) without creating an instance or starting a server. Run it in its own CI step, cached between runs, so the first test does not pay for the download:
await new PgServer(new PgServerOptions { PgVersion = "17", BinariesDir = "/ci-cache/pg" }).InstallAsync();
Named instances: keep a server around and reuse it
Give an instance a name to keep it, data included, and share it across runs, test projects and tools:
await using var server = new PgServer(new PgServerOptions { PgVersion = "17", InstanceName = "app" });
await server.StartAsync();
- A named instance lives in the shared cache (
instances/app), so every test run, app and tool on the machine finds the same one. PassDbDirto keep it somewhere else instead. - If it is already running, for example from an earlier run, another test project or the
pgembedCLI,StartAsync()attaches to it in milliseconds instead of starting another server (server.IsAttached). SetAttachIfRunning = falseto get an error instead. - Only the process that started a server stops it on dispose. A server you attached to keeps running for whoever started it.
StopAsync()andKillAsync()stop it either way. - Two processes starting the same name at once get one server: a lock per instance makes the second attach.
- Its data is kept when the server stops. Pin at least the major version (
"17", not"latest"): Postgres cannot open data from another major version, and the library refuses with a clear error before downloading anything.
Check or control a server from anywhere, including one another process started:
await using var app = new PgServer(new PgServerOptions { PgVersion = "17", InstanceName = "app" });
PgInstanceStatus status = await app.GetStatusAsync(); // State, ProcessId, Port, StartedAt
await app.StopAsync(); // clean shutdown
await app.KillAsync(); // last resort: force-kill
The shared cache
The shared cache is one folder per user: PGEMBED_CACHE_DIR if set, otherwise ~/.cache/pgembed on Linux (or $XDG_CACHE_HOME/pgembed), ~/Library/Caches/pgembed on macOS and %LOCALAPPDATA%\pgembed on Windows (PgCache.DefaultDirectory). It holds named instances, and their binaries in binaries.
Setting PGEMBED_CACHE_DIR also makes its binaries folder the default for every server, so all projects on the machine download each Postgres version once. Downloads are shared safely: when several processes need the same version at the same time, one downloads it and the others wait.
Available extensions
GetAvailableExtensionsAsync() lists everything CREATE EXTENSION can use in that install: the extensions bundled with Postgres plus any you configured. It reads the install folder, so the server does not need to be running:
foreach (var ext in await server.GetAvailableExtensionsAsync())
Console.WriteLine($"{ext.Name} {ext.DefaultVersion}: {ext.Comment}");
Options
Everything else is configured with PgServerOptions:
await using var server = new PgServer(new PgServerOptions
{
PgVersion = "17.6.0",
Port = 5432, // default: a free port, see server.PgPort
DbDir = "/tmp/my-tests", // default: the current directory
BinariesDir = "/tmp/pg-cache", // share downloads across projects (see below)
ServerParams = new Dictionary<string, string>
{
["max_connections"] = "300",
["timezone"] = "UTC",
["synchronous_commit"] = "off"
},
StartupTimeout = TimeSpan.FromSeconds(60),
Logger = loggerFactory.CreateLogger("Postgres")
});
await server.StartAsync();
| Option | Default | Description |
|---|---|---|
PgVersion |
required | Exact version (17.6.0) or a spec (17, 17.6, latest), see Choosing a version |
PgUser |
postgres |
Superuser created by initdb |
Port |
free port | TCP port; read the actual one from server.PgPort |
DbDir |
current directory | A pg_embed folder is created under it |
BinariesDir |
<DbDir>/pg_embed/binaries, or binaries in the shared cache for named instances and when PGEMBED_CACHE_DIR is set |
Cache of downloads and extracted installs |
InstanceName |
none | Keep and share an instance under a name, see Named instances |
AttachIfRunning |
true |
Attach to the instance's server when it is already running |
InstanceId |
new Guid | Older way to reuse an instance: prefer InstanceName (set one or the other) |
ClearInstanceDirOnStop |
true for a new instance, false for a named one |
Delete the instance folder when the server stops |
ClearWorkingDirOnStart |
false |
Delete the pg_embed folder before starting |
KeepRunningAfterExit |
false |
Keep the server running when the process exits without stopping it (by default it is killed, so tests never leave Postgres behind) |
ServerParams |
none | Postgres settings, written to a config file (values may contain spaces and quotes) |
Extensions |
none | Extension archives to install, see below |
Locale |
en_US.UTF-8 (system locale on Windows) |
Locale passed to initdb |
AddLocalUserAccessPermission |
false |
Windows only, see InitDb failure |
StartupTimeout / ShutdownTimeout |
30s / 10s | |
MavenRepo |
Maven Central | Use a mirror or an internal proxy |
DownloadRetryCount |
3 | Retries for each download |
DeleteFolderRetryCount / DeleteFolderInitialDelay |
5 / 16 ms | Retries when deleting folders that are briefly locked; the delay doubles each time |
HttpClient |
shared client | Configure proxies or custom handlers |
Logger |
none | Any Microsoft.Extensions.Logging logger |
Platform / Architecture |
detected | Override detection |
Extensions
The library downloads extension archives and extracts them into the Postgres install. Then run CREATE EXTENSION yourself.
Each archive must be a zip built for the same platform and Postgres major version as the server. A folder at the top of the zip that holds bin, lib and share is handled. For example, the PostGIS bundle below is a Windows build for Postgres 17: it extracts on Linux or macOS too, but CREATE EXTENSION then fails, so choose the extension URL per platform.
// Windows: PostGIS bundle for Postgres 17
await using var server = new PgServer(new PgServerOptions
{
PgVersion = "17.6.0",
Extensions =
[
new PgExtensionConfig("https://download.osgeo.org/postgis/windows/pg17/postgis-bundle-pg17-3.6.2x64.zip")
]
});
await server.StartAsync();
await using var conn = new NpgsqlConnection(server.ConnectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand("CREATE EXTENSION postgis", conn);
await cmd.ExecuteNonQueryAsync();
To make sure the archive is exactly the one you expect, give its SHA-256. A mismatch fails the start and the archive is not installed:
new PgExtensionConfig("https://example.com/my-extension.zip") { Sha256 = "9f86d081884c7d65..." }
Using it in xUnit tests
Downloading and extracting binaries takes time on the first run, so share one server across the tests in a class (or a whole assembly) with a fixture. This is xUnit v3, where IAsyncLifetime returns ValueTask; with xUnit v2, return Task from both methods instead:
public sealed class DatabaseFixture : IAsyncLifetime
{
public PgServer Server { get; } = new("17.6.0");
public async ValueTask InitializeAsync() => await Server.StartAsync();
public async ValueTask DisposeAsync() => await Server.DisposeAsync();
}
public class MyDbTests(DatabaseFixture db) : IClassFixture<DatabaseFixture>
{
[Fact]
public async Task your_test()
{
await using var conn = new NpgsqlConnection(db.Server.ConnectionString);
// ...
}
}
Good to know
postgresis the default database and the default superuser.- Trust authentication is used. Any password in the connection string is ignored. The server only listens on localhost, since the main use case is local tests.
- Each server gets an instance folder holding the
datafolder and the server log (server.LogFile): under<DbDir>/pg_embed, named afterInstanceId, or in the shared cache for a named instance. Unnamed instances are deleted when the server stops; named ones are kept. - To reuse an instance and its data, prefer
InstanceName. A fixedInstanceIdstill works. Either way the instance is set up on the first run and only started afterwards, and should pin at least the major version. PgPlatform.Detect()tells which platform and binaries the library uses on this machine.- If the test process exits without disposing the server, the library makes a best effort to kill the server on process exit.
How it works
- A version spec such as
17is resolved against the versions published for the platform. The zonky binaries for that version are downloaded from Maven once, checked against the SHA-256 Maven publishes next to them, and cached inBinariesDir. Each download is retried 3 times. An unknown version fails fast with a clear error. - The binaries, plus the configured extensions, are extracted once into a shared install folder in
BinariesDir, for examplepostgres-17.6.0-linux-amd64. Every instance with the same version and extensions reuses it, so later servers start in about a second. initdbcreates the instancedatafolder.- The port and
ServerParamsare written topg_embed.conf, which is included frompostgresql.conf. pg_ctl start -wstarts the server and waits until it accepts connections.- On stop,
pg_ctl stopruns in fast mode, falling back to immediate mode and then to killing the process.
Breaking changes in v5
v5 modernizes the library. Summary of what changed for callers:
- Target frameworks: .NET 8, 9, 10 and 11. .NET 6 and 7 (out of support) are dropped.
- Async only:
Start(),Stop()andDispose()are removed. Useawait StartAsync(),await StopAsync()andawait using.PgServerimplementsIAsyncDisposableonly. - Options object: the long constructor is replaced by
new PgServer(pgVersion)andnew PgServer(PgServerOptions). The parameters map to option properties of the same name.startupWaitTime(ms) is nowStartupTimeout(TimeSpan),pgServerParamsisServerParams,pgExtensionsisExtensions,deleteFolderRetryCountisDeleteFolderRetryCount, anddeleteFolderInitialTimeout(ms) isDeleteFolderInitialDelay(TimeSpan).deleteFolderTimeoutFactoris gone: the delay always doubles. - Instance cleanup: instances without an
InstanceIdare now deleted on stop by default. SetClearInstanceDirOnStop = falseto keep them. - Folder layout: binaries now live in a shared install folder under
BinariesDir, soPgBinDirpoints there instead of<instance>/bin. Existing named instances keep their data. - Removed:
PgExtensionConfig.CreateExtensionSqlList(it was never executed). RunCREATE EXTENSIONyourself. - Errors: failures are reported as
PostgresEmbedException, and startup errors include the end of the server log. - New:
ConnectionString,IsRunning,LogFile,InstallDir,CancellationTokensupport, logging, proxy support throughHttpClient, Linux arm64 and Alpine support, and support for paths with spaces and non-ASCII characters.
Known issues
InitDb failure while starting embedded server
initdb: could not change permissions of directory "./pg_embed/aa60c634-fa20-4fa8-b4fc-a43a3b08aa99/data": Permission denied
All Postgres processes run under the current account, and Postgres needs full access to the parent folder of the data directory. On Windows, set AddLocalUserAccessPermission = true. The library then runs the following before initdb:
icacls.exe <instance folder> /t /grant:r <user>:(OI)(CI)F
The account needs the right to change folder permissions. You usually only need this on CI agents.
Non-ASCII paths on Windows
Postgres on Windows cannot use folder paths with characters outside the system code page, such as Chinese folder names. The library works around this by handing Postgres the 8.3 short form of the path, which is ASCII only. Short names are on by default for the system drive, but are often turned off on other drives.
Without them, startup stops before downloading anything, with an error that lists the problem characters and the two ways out:
- Set
DbDir(orBinariesDir, whichever the error names) to a folder whose full path is ASCII only, such asC:\pg-embed. - Turn on short names for that drive as an administrator (
fsutil 8dot3name set D: 0), then delete and recreate the folder. Folders created while short names were off never get one.
InitDb failure with a large negative number
If initdb fails on Windows with a large negative exit code, install the latest Microsoft Visual C++ Redistributable.
Acknowledgements
- This project uses the minimal Postgres binaries published by zonkyio/embedded-postgres-binaries.
- Yandex Embedded PostgreSQL and OpenTable Embedded PostgreSQL Component were useful references while designing the implementation.
License
MysticMind.PostgresEmbed is licensed under the MIT License. Refer to the License file for more information.
Copyright © 2023-2026 Babu Annamalai
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- CliWrap (>= 3.10.5)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Polly.Core (>= 8.9.0)
- SharpCompress (>= 0.50.8)
-
net11.0
- CliWrap (>= 3.10.5)
- Polly.Core (>= 8.9.0)
- SharpCompress (>= 0.50.8)
-
net8.0
- CliWrap (>= 3.10.5)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Polly.Core (>= 8.9.0)
- SharpCompress (>= 0.50.8)
-
net9.0
- CliWrap (>= 3.10.5)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Polly.Core (>= 8.9.0)
- SharpCompress (>= 0.50.8)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on MysticMind.PostgresEmbed:
| Package | Downloads |
|---|---|
|
Aero.Cms.Db.Marten
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.2.1 | 74 | 11/6/2026 |
| 5.2.0 | 41 | 11/6/2026 |
| 5.1.0 | 41 | 11/6/2026 |
| 5.0.0 | 47 | 11/6/2026 |
| 4.0.0 | 650,465 | 12/14/2023 |
| 3.1.0 | 21,202 | 8/30/2023 |
| 3.0.1 | 546 | 8/13/2023 |
| 3.0.0 | 7,783 | 8/12/2023 |
| 2.1.0 | 5,408 | 2/24/2023 |
| 2.0.0 | 7,666 | 8/30/2022 |
| 1.5.0 | 54,419 | 8/31/2020 |
| 1.4.0 | 25,014 | 9/1/2019 |
| 1.3.0 | 5,698 | 3/15/2019 |
| 1.2.0 | 1,306 | 2/20/2019 |
| 1.1.3 | 3,182 | 12/20/2018 |
| 1.1.2 | 1,339 | 10/8/2018 |
| 1.1.1 | 1,263 | 9/10/2018 |
| 1.1.0 | 2,490 | 8/29/2018 |
| 1.0.0 | 2,291 | 6/28/2018 |
| 0.1.20 | 3,932 | 2/17/2017 |