Make the test PostgreSQL/MySQL server skip durability work
Speed up database-heavy tests by running the test PostgreSQL or MySQL server with fsync=off, synchronous_commit=off and a tmpfs data directory, in Docker or GitHub Actions.
A production database promises that a committed transaction survives a crash or a power cut. To keep that promise, PostgreSQL and MySQL write every commit to a log and wait for the disk to confirm it with fsync() before they answer. That wait is often the slowest part of a small write.
A test database doesn't need any of that. It's thrown away at the end of the run (or rebuilt from migrations), and nobody cares what's left of it after a crash. Tell the server so, and creating the test database, running migrations, and every INSERT and COMMIT in your tests stop waiting on the disk.
This only changes how the server writes to disk. Queries, transactions, constraints and locking behave exactly the same.
What the server is waiting for
- fsync on commit. Every commit flushes the write-ahead log (PostgreSQL) or redo log (InnoDB) to disk and waits for it. Tests commit a lot: migrations,
TransactionTestCaseflushes, fixtures that commit, and every write made in autocommit mode. - Checkpoints and full-page writes. PostgreSQL periodically flushes data files and, after each checkpoint, writes whole pages to the WAL to guard against torn pages.
- The binary log. MySQL 8 writes every change to the binary log for replication by default.
- Disk I/O at all. On a CI runner or a laptop the data directory sits on a real disk, often a slow virtual one.
PostgreSQL settings
The PostgreSQL docs on non-durable settings list what's safe to turn off when you don't need durability. For a test server:
| Setting | What it skips |
|---|---|
fsync=off |
flushing data and WAL to disk |
synchronous_commit=off |
waiting for the WAL write on every commit |
full_page_writes=off |
writing whole pages to the WAL after checkpoints |
| data directory on tmpfs | disk I/O altogether: the data lives in RAM |
fsync and full_page_writes can only be set in the server config or on its command line, so pass them when you start the server. The official postgres image passes extra arguments straight to the server.
docker run
$ docker run -d --name test-postgres \
-e POSTGRES_PASSWORD=postgres -p 5432:5432 \
--tmpfs /var/lib/postgresql \
postgres:18 -c fsync=off -c synchronous_commit=off -c full_page_writes=off
Since PostgreSQL 18 the image keeps its data under /var/lib/postgresql (in /var/lib/postgresql/18/docker). For PostgreSQL 17 and older, mount the tmpfs at /var/lib/postgresql/data instead.
docker compose
services:
postgres:
image: postgres:18
command: ["-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
environment:
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
tmpfs:
- /var/lib/postgresql
Check that it took effect:
$ psql -h localhost -U postgres -c "SHOW fsync" -c "SHOW synchronous_commit"
GitHub Actions service container
Service containers accept a command, passed to the container like the arguments after the image name in docker run, and options for extra docker create flags such as --tmpfs:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
command: -c fsync=off -c synchronous_commit=off -c full_page_writes=off
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--tmpfs /var/lib/postgresql
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v5
# set up Python and install your dependencies here
- run: pytest
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
command on service containers is a fairly recent addition. If your runner rejects it (an older GitHub Enterprise Server, or another CI system whose services can't take arguments), start the database in a step with the docker run command above instead, and wait for pg_isready before running the tests.
Only synchronous_commit on a shared server
If you can't restart the server (a shared dev database), synchronous_commit can still be turned off per connection, for the test connections only. With Django:
# settings/test.py
DATABASES["default"].setdefault("OPTIONS", {})["options"] = "-c synchronous_commit=off"
That risks losing the last few commits if the server crashes, never corruption, and leaves other clients alone.
MySQL settings
| Option | What it skips |
|---|---|
--innodb-flush-log-at-trx-commit=0 |
writing and flushing the redo log on every commit (it's flushed about once a second instead) |
--skip-log-bin |
the binary log (on by default in MySQL 8) |
| data directory on tmpfs | disk I/O altogether |
services:
mysql:
image: mysql:8.4
command: --innodb-flush-log-at-trx-commit=0 --skip-log-bin
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test_myapp
ports:
- "3306:3306"
tmpfs:
- /var/lib/mysql
The same command works in a GitHub Actions service container, with --tmpfs /var/lib/mysql in options.
Keep the tests correct
- Never on a server with data you need. With
fsync=offan operating system crash or power loss can corrupt the whole cluster, and a tmpfs data directory is gone when the container stops. Run a separate, throwaway server for tests. - It only affects crash safety of the throwaway test server; query behaviour, isolation and constraints are identical, so tests stay as meaningful as before.
- If you reuse the test database (
--reuse-db/--keepdb) with a tmpfs data directory, the kept database survives only as long as the container. That's fine in CI; locally, use a disk-backed volume if you want the database to survive restarts. - Keep the same database engine and major version as production. A fast server that differs from production hides real bugs; SQLite instead of PostgreSQL is a different database, not a faster setting.
How flakytest finds it
flakytest adds up the time tests spend in the database, schema setup and migrations. When your Django settings use PostgreSQL or MySQL and that time is at least 5 seconds and 20% of test time, it suggests Make the test database server skip durability work. It estimates the saving conservatively, as a fraction of the database time, because how much a server waits on the disk depends on the machine. Measure it yourself: profile a run, restart the server with these settings, profile again and run flakytest compare.
Further reading
- PostgreSQL: non-durable settings
- PostgreSQL: WAL configuration (
fsync,synchronous_commit,full_page_writes) - MySQL:
innodb_flush_log_at_trx_commit - Docker Hub: the official postgres image
- GitHub Actions:
services.<service_id>.command
Related guides
- Stop re-running migrations on every test run: Django runs every migration before the first test, on every run. Keep the test database with --reuse-db or --keepdb, or skip migrations with --nomigrations.
- Run migrations once with pytest-xdist and CI shards: With pytest-xdist every worker builds its own Django test database, and every CI shard does too. Migrate once and copy it with PostgreSQL template databases or pg_dump.
- Stop opening new database connections in tests: Tests that create an engine per test, use NullPool or close Django connections reconnect to the database over and over. Create the engine once per session and keep connections open.
- Speed up pytest in CI: Speed up slow CI test jobs: cache dependencies with uv or pip, use pytest-xdist on every core, shard with pytest-split, migrate once, and see why CI is slower than local.
Updated October 18, 2026 · Suggest a change