+ We are excited to announce that + Torrust Tracker + now supports + PostgreSQL + as a first-class database backend, alongside the existing + SQLite and + MySQL drivers. + This has been a long-standing feature request, and it is the result of a long journey that involved + a full overhaul of the persistence layer, new tooling, and valuable contributions from the community. +
++ In this post we will walk through the history of this feature, explain what changed under + the hood, highlight the new tooling built along the way, and share our plans for the + upcoming major release. +
+ ++ The first request for PostgreSQL support was filed back on September 20, 2023 + in + issue #462. PostgreSQL is widely used in production environments and it was a natural fit for + operators who already run PostgreSQL infrastructure and want to avoid introducing a second + database engine. +
+
+ At the time, however, adding PostgreSQL support was not straightforward. The tracker's
+ persistence layer was synchronous, using the
+ r2d2
+ connection pool crate with
+ rusqlite
+ and
+ mysql. The problem is that the
+ postgres
+ crate wraps
+ tokio_postgres, which tries to spawn a nested
+ Tokio runtime —
+ something that conflicts with the tracker's existing async runtime. The community
+ PR #1684 worked around this by spawning a fresh OS thread per database operation, but that approach
+ was rejected as a performance concern: under load, each query would pay the cost of OS thread
+ creation and context switching.
+
+ The real fix was to migrate all drivers to an async-native library. Rather than bolt
+ PostgreSQL on top of the synchronous stack with a workaround, we decided to do the right
+ thing first: replace r2d2 / rusqlite / mysql with
+ async
+ sqlx across all drivers, and then add PostgreSQL on top of that clean foundation.
+
+ In May 2025 we opened + EPIC #1525 — Overhaul persistence, which became the umbrella for all the work needed before PostgreSQL could be added + cleanly. The driving insight was the + Martin Fowler quote we kept returning to: +
+"Make the change easy, then make the easy change."+
+ The EPIC was broken down into two phases and nine sequential sub-issues, each
+ independently reviewable and mergeable into develop.
+
Before touching a single PostgreSQL file, we had to modernise the persistence stack:
+persistence_benchmark_runner) that measures per-operation latency for each driver using --driver,
+ --db-version, and --ops flags, and outputs a JSON report for easy
+ diffing across runs.
+ Database trait
+ was split into four narrow context traits (SchemaMigrator,
+ TorrentMetricsStore, WhitelistStore,
+ AuthKeyStore) plus a blanket aggregate supertrait, reducing coupling and
+ making each driver easier to implement and test in isolation.
+ sqlx connection pools, replacing the old synchronous
+ r2d2 / rusqlite / mysql stack.
+ sqlx::migrate!(), and a legacy-bootstrap path was added to history-align
+ databases that were created before migrations existed.
+ INTEGER (signed 32-bit, max ~2.1 billion) to
+ BIGINT via a versioned migration. The Rust type
+ NumberOfDownloads stays u32 — the wider column is intentional; the
+ application type bounds writes at compile time.
+ + With the foundation in place, adding PostgreSQL (sub-issue 1525-08, + tracked in + issue #1723) became straightforward: +
+Driver::PostgreSQL variant in the configuration crate (serialises as
+ "postgresql").
+ postgres.rs driver implementing all four narrow traits via async
+ sqlx.
+
+ A key part of this story is the contribution from community member
+ DamnCrab. They opened
+ PR #1684
+ with an initial PostgreSQL implementation using
+ r2d2_postgres, which started the conversation about the right approach. Their follow-up
+ PR #1695
+ incorporated review feedback, and reviewer guidance was provided in
+ PR #1700
+ to help bridge the gap.
+
Three ideas from DamnCrab's work were retained in the final implementation:
+sqlx migration and across database engines.
+ + Thank you, DamnCrab, for the contributions and ideas that made their way into the final + result. +
+ ++ The overhaul produced several tools that are useful beyond just adding PostgreSQL support: +
+
+ A
+ GitHub Actions workflow (db-compatibility.yaml)
+ that runs the tracker's full driver test suite against a matrix of database versions on every
+ push and pull request:
+
+ Each combination spins up a real container via
+ testcontainers
+ and runs the driver tests with the db-compatibility-tests feature flag. This explicitly
+ documents which database versions the tracker is compatible with, and protects users who run
+ infrastructure with a version that differs from what the developers typically test against.
+ If a new database release introduces a breaking change, the matrix catches it before it reaches
+ users.
+
+ A + Rust + binary that runs a complete BitTorrent transfer — seeder announces, leecher connects, download + completes — using real + qBittorrent clients running in containers. This is the closest we can get to a real-world integration + test without deploying to production. +
+
+ The
+ persistence_benchmark_runner
+ is a developer binary that measures the persistence-layer operations implemented by the
+ Database trait. It benchmarks one driver per invocation and prints a JSON
+ report to standard output with per-operation timing statistics: count,
+ best,
+ median, and worst in microseconds.
+
Run it with:
++ The output is plain JSON so you can redirect it to a file, diff runs, or feed it into any + visualisation tool: +
+A sample report looks like this:
++ It is intentionally simple — there is no built-in comparison mode. The value comes from + running it before and after a change (or across drivers) and diffing the JSON files. This + makes it easy to catch performance regressions early, without the overhead of a full + criterion benchmark suite. +
+
+ There is also a
+ GitHub Actions workflow (db-benchmarking.yaml)
+ that runs the benchmark against all three drivers on every push and pull request. It runs with
+ --ops 10 — just enough to confirm the binary builds and executes cleanly — rather
+ than producing statistically significant numbers. The main purpose in CI is to catch compilation
+ breakages and driver-level errors early, not to track performance over time.
+
+ With all three drivers in place we ran the benchmark runner for the first time across all
+ engines on the same machine (AMD Ryzen 9 7950X, Ubuntu 25.10, Docker 28.3.3) with
+ --ops 100. The full report is available in the repository at
+ docs/benchmarking/runs/2026-05-01/REPORT.md.
+
| Driver | +Total (ms) | +
|---|---|
| SQLite3 | 119 ms |
| MySQL 8.4 | 6 372 ms |
| MySQL 8.0 | 7 272 ms |
| PostgreSQL 17 | 1 451 ms |
| Operation | +SQLite3 | +MySQL 8.4 | +MySQL 8.0 | +PostgreSQL 17 | +
|---|---|---|---|---|
| save_torrent_downloads | 89 | 769 | 984 | 298 |
| load_torrent_downloads | 23 | 112 | 115 | 88 |
| load_all_torrents_downloads | 77 | 172 | 171 | 146 |
| increase_downloads_for_torrent | 70 | 773 | 1 005 | 302 |
| save_global_downloads | 76 | 793 | 1 066 | 299 |
| load_global_downloads | 21 | 115 | 137 | 86 |
| increase_global_downloads | 67 | 774 | 1 036 | 305 |
| add_info_hash_to_whitelist | 81 | 735 | 981 | 294 |
| get_info_hash_from_whitelist | 21 | 109 | 118 | 95 |
| load_whitelist | 55 | 161 | 175 | 135 |
| remove_info_hash_from_whitelist | 81 | 766 | 962 | 293 |
| add_key_to_keys | 81 | 750 | 974 | 292 |
| get_key_from_keys | 22 | 118 | 129 | 95 |
| load_keys | 77 | 167 | 189 | 155 |
| remove_key_from_keys | 73 | 739 | 994 | 300 |
load_*) are slightly slower on PostgreSQL.
+ + These numbers are the first PostgreSQL baseline. Future runs will track regressions as the + persistence layer continues to evolve, and will provide a growing picture of how the + drivers compare over time. +
+ +
+ PostgreSQL support is already merged into the develop branch and will be
+ included in the next major release (v4.0.0), which we are planning to
+ ship this year — though no date is set yet. If you want to try it out today, you can build
+ from
+ develop and set the driver in your tracker configuration:
+
+ A default Docker Compose configuration for PostgreSQL is also included in the repository, + so you can spin up a local instance with a single command. +
+