PostgreSQL on DigitalOcean vs ServersCamp. Droplets, volumes and a managed database
We keep testing our storage with PostgreSQL, and this time we compared it with DigitalOcean. It is the cloud a lot of small teams start on, and the first place many of them put a PostgreSQL database. So we took the droplet people buy for that job, gave it the same database we give ours, and ran the same pgbench battery on both.
On the DigitalOcean side that is a Basic Regular droplet with 4 vCPU and 8 GB for $48 a month. On ours it is a bs-l with the same 4 vCPU and 8 GB and a 160 GB W2 disk, our entry database class, for EUR 39.97 a month including the IPv4 address. Both are the budget line of their cloud and the size you pick when the database stops being a side project. At current exchange rates the two monthly checks are a few dollars apart.
We ran two scenarios, because DigitalOcean gives you two ways to host that database.
The first is a plain instance. You rent a server and install PostgreSQL yourself. On DigitalOcean the data can live on the droplet's own disk or on a Volume, their network block storage, so we tested both. On our side the data lives on W2 in both cases.
The second is managed. DigitalOcean sells Managed PostgreSQL, and we put its 4 vCPU / 8 GB node against what you can build on ServersCamp by hand, one VM with the database and one VM with the application, talking over our private network. We do not sell managed PostgreSQL yet. Quietly, it is already in internal testing, and until it ships the fair comparison is the setup a customer can build today.
| Scenario | DigitalOcean | ServersCamp |
|---|---|---|
| 1a. Instance, local disk | Droplet s-4vcpu-8gb with 160 GB local SSD, $48 | bs-l with 160 GB W2, EUR 39.97 |
| 1b. Instance, network disk | Same droplet plus a 160 GB Volume, $64 | bs-l with 160 GB W2, EUR 39.97 |
| 2. Managed vs by hand | Managed PostgreSQL 4 vCPU / 8 GB, 160 GiB, no standby, $126.40 | Database VM bs-l with W2, EUR 39.97, application VM next to it |
The contenderstop
| DigitalOcean | ServersCamp | |
|---|---|---|
| Instance | Basic Regular s-4vcpu-8gb, 4 vCPU / 8 GB, shared CPU | bs-l Sustained, 4 vCPU / 8 GB |
| CPU | Intel, model hidden by the provider (Broadwell family, 2016) | Intel Xeon Skylake (2017) |
| Local disk | 160 GB SSD | no |
| Network disk | Volume 160 GB, $16, replicated | W2 160 GB, EUR 15.47, replicated across nodes, power-loss protected, 25k IOPS and 500 MiB/s |
| Managed database | 4 vCPU / 8 GB node, 160 GiB SSD, $126.40, one standby adds $126.40 | not sold yet |
| Region | FRA1 | eu-east-ro-1 |
| OS / PostgreSQL | Ubuntu 26.04, PostgreSQL 18.6 from the Ubuntu archive, same pgtune formula on every server | |
Methodtop
PostgreSQL 18.6 from the Ubuntu 26.04 archive, the same pgtune formula on every server (shared buffers at a quarter of RAM, work memory and parallel workers derived from the box itself). fsync and synchronous_commit stayed on everywhere, including the managed database, where we had to turn synchronous_commit back on. Each database size was loaded with pgbench -i using server-side generation, then measured with three 120 second TPC-B write runs and three 120 second select-only read runs at 32 connections and 4 threads. The ladder was scale 100, 1000 and 6500, which is 1.5, 15 and 95 GB against 8 GB of RAM. In scenario 1 pgbench ran on the database server. In scenario 2 it ran on a separate VM in the same private network, the way an application talks to its database. Every run is reported as it came out. The commands are at the bottom of the page.
Scenario 1. A plain instancetop
Writes
TPC-B, three UPDATEs, one SELECT and one INSERT per transaction, 32 connections.
| Database | DO local disk | DO Volume | ServersCamp W2 |
|---|---|---|---|
| 1.5 GB | 2 477 tps (12.3 to 13.6 ms) | 2 254 tps (13 to 16 ms) | 6 289 tps (5.1 ms) |
| 15 GB | 2 427 tps (12.6 to 13.5 ms) | 1 154 tps (27 to 29 ms) | 4 892 tps (6.5 ms) |
| 95 GB | 1 950 tps (14.9 to 17.8 ms) | 824 tps (32 to 47 ms) | 4 377 tps (7.2 to 7.4 ms) |
The worst 10 seconds of each run show the same. At 15 GB the Volume dropped to 484 tps, the local disk to 1 985 and W2 to 4 608. At 95 GB the numbers were 455, 1 478 and 4 180.
The local disk doubles DigitalOcean's write rate compared with its Volume on the larger databases, and the droplet is still about half as fast as W2. On the small database both DigitalOcean disks land in the same place, because the database fits in RAM and the CPU sets the pace. Ours got two and a half times more transactions out of the same four cores, with no steal.
Reads
Select-only point lookups, 32 connections.
| Database | DO local disk | DO Volume | ServersCamp W2 |
|---|---|---|---|
| 1.5 GB | 26 429 tps (1.2 ms) | 25 642 tps (1.2 to 1.4 ms) | 50 980 tps (0.6 ms) |
| 15 GB | 13 385 tps (2.3 to 2.5 ms) | 2 752 tps (9.7 to 15.7 ms) | 17 891 tps (1.7 to 1.9 ms) |
| 95 GB | 10 655 tps (2.8 to 3.3 ms) | 2 107 tps (13 to 17 ms) | 13 521 tps (2.4 ms) |
Here the droplet's local disk helps. It reads the 95 GB database five times faster than the Volume, and the gap to W2 shrinks to 1.3x. W2 is capped at 25k IOPS and the 95 GB read runs used about 22k of them, so a larger class of ours would read faster, at a higher price. On the Volume the droplet's CPU spent more than half its time waiting for the disk during large reads (iowait 52 to 60%).
A single commit
One client inserting one row per transaction for 60 seconds. Every commit waits until the WAL is on durable media, so this is the time your application waits on every write.
| DO local disk | DO Volume | ServersCamp W2 | |
|---|---|---|---|
| Commits per second | 533 | 573 | 3 185 |
| Latency per commit | 1.87 ms | 1.74 ms | 0.31 ms |
| pg_test_fsync, fdatasync of 8 kB | 1 529 µs | 1 977 µs | 249 µs |
The local disk commits no faster than the Volume. A synchronous write costs about 1.5 ms on DigitalOcean whichever disk you choose, and money does not buy it lower, since neither disk has performance tiers. W2 confirms a write in a quarter of a millisecond after it has landed on independent storage nodes.
Loading and restoring the database
| 95 GB database | DO local disk | DO Volume | ServersCamp W2 |
|---|---|---|---|
| Initial load (pgbench -i) | 41 min 20 s | 1 h 6 min | 27 min 11 s |
| pg_dump -Fd -j 4 | 11 min 34 s | 13 min 1 s | 6 min 31 s |
| pg_restore -j 4 | 39 min 13 s | 47 min 59 s | 21 min 57 s |
| Dump plus restore | 50 min 47 s | 1 h 1 min | 28 min 28 s |
Both the dump and the restore of one large table are mostly single-threaded work, one core compressing or one core running COPY and building the primary key, so the faster core decides the result and the local disk barely helps.
Scenario 2. Managed PostgreSQL vs a database you build by handtop
Here pgbench runs on a separate VM and reaches the database over the private network, which is how an application sees it. On DigitalOcean the client was a droplet in the same VPC as the managed cluster, connected with the VPC hostname from their control panel on the direct port. On ServersCamp the client was a second bs-l on a different host in the same private network. Both sides used TLS 1.3.
This scenario compares the SQL speed an application gets. It does not compare the services. DigitalOcean Managed PostgreSQL includes daily backups, point-in-time recovery, minor version updates and node replacement, and with a database you build by hand those are your job. Prices in this article are the price of the database server alone. The client VM is left out on both sides.
| Database | DO Managed PostgreSQL | ServersCamp, by hand |
|---|---|---|
| 1.5 GB load | 34.6 s | 18.2 s |
| 1.5 GB writes | 1 215 tps (24.7 to 28.1 ms) | 5 340 tps (5.9 to 6.1 ms) |
| 1.5 GB reads | 9 538 tps (2.9 to 3.7 ms) | 37 848 tps (0.84 ms) |
| 15 GB load | 9 min 30 s | 4 min 38 s |
| 15 GB writes | 858 tps (34.7 to 38.9 ms) | 4 377 tps (7.3 ms) |
| 15 GB reads | 4 904 tps (6.3 to 6.8 ms) | 18 527 tps (1.7 to 1.8 ms) |
| 95 GB load | 1 h 5 min | 28 min 46 s |
| 95 GB writes | 684 tps (40.9 to 53.4 ms) | 3 914 tps (8.1 to 8.3 ms) |
| 95 GB reads | 5 038 tps (5.5 to 7.3 ms) | 13 468 tps (2.4 ms) |
The managed database was also the least steady system in the whole test. In one 15 GB write run it fell to 94 transactions per second for ten seconds, and its latency standard deviation ran higher than its average. On our side the worst 10 seconds of any write run stayed within 7% of the average.
DigitalOcean's own monitoring shows that the managed node was not fully busy. During the 1.5 GB runs its CPU stayed between 55 and 74%, while our database server had 3% idle. At 32 clients and about 3 ms per read, 32 connections can complete roughly 10 thousand reads per second, which is close to what we measured. That fits the network between the client and the managed node being the limit. We did not measure where inside their service the time went, so this is our reading of the numbers and not a measured cause.
Going over the network cost our setup 11 to 16% on writes and 26% on small reads compared with running pgbench on the database server itself. On large reads it came out even, since the database server no longer shared its CPU with the client.
synchronous_commit was off on our DigitalOcean Managed PostgreSQL clustertop
The cluster we created in FRA1 on 27 September 2026 answered SHOW synchronous_commit with off right after creation. We did not change it, and no setting on the database or the role set it. We checked one cluster, so treat this as what we saw there, and check your own.
SELECT name, setting, source, boot_val, reset_val
FROM pg_settings WHERE name = 'synchronous_commit';
synchronous_commit | off | configuration file | on | off
boot_val is PostgreSQL's own default, which is on. On this cluster the off came from the configuration file. With off, the server tells the client that a transaction is committed before its WAL reaches the disk. If the node crashes or restarts in that window, transactions the application already considers saved are gone, and nothing reports an error. With the default wal_writer_delay of 200 ms the window is up to about 600 ms of writes.
We ran every managed test with ALTER DATABASE bench SET synchronous_commit = on, so both sides pay for durability. Then we measured what the default buys.
| One client, one INSERT per transaction, over the network | Commits per second | Latency |
|---|---|---|
| DO Managed, synchronous_commit on | 476 | 2.10 ms |
| DO Managed, synchronous_commit off (as the cluster came) | 1 508 | 0.66 ms |
| ServersCamp, synchronous_commit on | 2 461 | 0.41 ms |
With off their single-client commits were three times faster, and a durable commit on ServersCamp was still faster than a non-durable one on DigitalOcean. If you run DigitalOcean Managed PostgreSQL, check this setting on your cluster. It can be changed per database with the statement above.
What the monitoring and fio showedtop
We watched both sides while the tests ran. On our VM the platform records CPU, disk throughput and IOPS every 2 seconds. On every machine vmstat wrote a line every 5 seconds. DigitalOcean shows its own graphs in the control panel.
ServersCamp VM, platform metrics per phase
W2 limits are 25k IOPS and 500 MiB/s. CPU is the share of all four vCPU.
| Phase | CPU avg / p95 | Disk read MB/s, avg / p95 | Disk write MB/s, avg / p95 | IOPS read / write | Limited by |
|---|---|---|---|---|---|
| Load 1.5 GB | 11 / 27% | 0 | 96 / 409 | 0 / 152 | writing |
| Writes 1.5 GB | 80 / 100% | 0 | 12 to 23 / 58 | 0 / 950 | CPU |
| Reads 1.5 GB | 90 / 100% | 0 | 5 | 0 / 23 to 79 | CPU |
| Load 15 GB | 27 / 52% | 87 / 499 | 299 / 458 | 239 / 238 | W2 bandwidth |
| Writes 15 GB | 81 / 100% | 46 to 51 / 89 | 72 to 79 / 164 | 5 000 / 5 400 | CPU, disk at 20 to 25% of IOPS |
| Reads 15 GB | 90 / 100% | 193 to 216 / 255 | 12 to 80 | 20 000 to 22 000 / 900 to 6 800 | CPU and 88% of W2 IOPS |
| Load 95 GB | 29 / 47% | 120 / 501 | 296 / 408 | 131 / 233 | W2 bandwidth |
| Writes 95 GB | 81 / 100% | 51 to 53 / 72 | 96 to 105 / 190 | 6 400 / 6 500 | CPU |
| Reads 95 GB | 90 / 100% | 175 to 178 / 207 | 18 to 33 | 22 000 / 1 200 to 3 000 | CPU and 88% of W2 IOPS |
| Single-client commits | 32 / 41% | 0 | 21 to 25 | 0 / 2 600 to 3 300 | one synchronous write at a time |
| pg_dump 95 GB | 55 / 65% | 210 / 226 | 41 / 89 | 115 / 2 129 | one core compressing |
Writes stayed on the CPU at every size, with the disk at a quarter to a third of its limits. Large reads used 22k of the 25k IOPS, so this is the one place where a higher class would change the result.
vmstat per phase, scenario 1
Averages of user, system, iowait and steal in percent, over each run and the pause after it.
| Phase | DO local disk | DO Volume | ServersCamp W2 |
|---|---|---|---|
| Writes 1.5 GB | 37 to 40 / 21 to 22 / 3 to 4 / 5 to 7 | 31 to 39 / 19 to 21 / 3 to 4 / 6 to 14 | 45 / 33 / 0 / 0 |
| Reads 1.5 GB | 62 / 25 to 26 / 0 / 1 | 60 to 63 / 26 to 27 / 0 / 0 to 4 | 57 to 61 / 28 to 32 / 0 / 0 |
| Writes 15 GB | 39 to 40 / 25 to 26 / 6 to 7 / 1 to 3 | 20 to 23 / 14 to 15 / 27 to 28 / 9 to 13 | 40 / 33 to 34 / 4 / 0 |
| Reads 15 GB | 45 to 47 / 37 to 38 / 4 to 5 / 1 to 2 | 11 to 17 / 11 to 14 / 52 to 56 / 6 to 9 | 33 to 35 / 36 to 37 / 18 to 19 / 0 |
| Writes 95 GB | 32 to 37 / 23 to 25 / 9 to 13 / 3 to 6 | 14 to 19 / 10 to 13 / 30 to 39 / 12 to 15 | 38 to 39 / 33 to 34 / 5 to 6 / 0 |
| Reads 95 GB | 38 to 42 / 36 to 38 / 7 to 12 / 1 to 3 | 11 to 15 / 11 to 14 / 55 to 60 / 6 to 11 | 28 / 31 / 30 / 0 |
Steal on the droplet reached 25% in single samples during write runs and 35% during Geekbench. On our VM it was zero in every sample of every test.
DigitalOcean control panel graphs
On the droplet with local disk, loading the 15 GB database peaked at 230 MB/s of writes and 175 MB/s of reads, while fio measured 1 348 MB/s of sequential writes on the same disk. During the 15 GB write runs the disk moved 40 to 55 MB/s and the CPU climbed to 80%. During the 1.5 GB read runs the CPU was at 88 to 100% with a load average of 30 on four vCPU.
On the managed cluster the CPU stayed between 55 and 74% through the 1.5 GB runs, with a load average of 12 to 19. The client droplet was 9 to 16% busy. On our side the database server sat at 3% idle during the same tests and the client used 11 to 28%.
fio
fio 3.41, 8 GiB file, direct IO, 30 seconds per test after 5 seconds of warm-up.
| Test | DO local disk | DO Volume | ServersCamp W2 |
|---|---|---|---|
| 4k random read, one at a time | 298 µs | 1 229 µs | 223 µs |
| 4k random write, one at a time, p99 | 1 237 µs | 897 µs | 257 µs |
| 4k write with fsync, one at a time | 350 per second | 217 per second | 2 920 per second |
| 4k random read, 8 jobs x 64 deep | 49 221 IOPS | 4 869 IOPS | 24 986 IOPS (class limit) |
| 4k random write, 8 jobs x 64 deep | 34 353 IOPS | 7 450 IOPS | 25 007 IOPS (class limit) |
| 1 MB sequential read, 4 jobs | 3 507 MB/s | 420 MB/s | 501 MB/s (class limit) |
| 1 MB sequential write, 4 jobs | 1 348 MB/s | 301 MB/s | 500 MB/s (class limit) |
The droplet's local disk has more raw throughput than W2 and its large sequential streams are seven times faster. W2 stops at the limits of its class by design, and higher classes raise them. In the PostgreSQL tests above that throughput did not turn into speed, because the database waited on single synchronous writes and on the CPU.
Where the time goestop
- CPU. The droplet's vCPU is shared, and steal showed up in most samples, up to 25% during write runs and 35% during Geekbench. Our vCPU showed zero steal in every phase of every test. On an in-memory database both servers were CPU-bound and ours did more work per second.
- Synchronous writes. Every commit waits for one synchronous write. On DigitalOcean that write costs about 1.5 ms on the local disk and about 2 ms on the Volume. On W2 it costs 0.25 ms. With 32 clients the database batches commits and the difference shrinks, which is why the write gap is two to five times while the single-client gap is six.
- Disk under load. The Volume is the slowest part of the DigitalOcean stack. At 15 and 95 GB it kept the droplet's CPU waiting more than half the time on reads. W2 held 22k of its 25k IOPS with the CPU still busy.
- The network to the managed database. A TPC-B transaction is several round trips, and on the managed side each one cost about 0.66 ms against 0.2 to 0.5 ms on our private network. With 32 clients those round trips could be enough to cap the throughput on their side. We did not isolate it.
Per eurotop
| Scenario | DigitalOcean | ServersCamp | 95 GB writes, DO vs ServersCamp |
|---|---|---|---|
| Instance, local disk | $48 | EUR 39.97 | 1 950 vs 4 377 tps |
| Instance, Volume | $64 | EUR 39.97 | 824 vs 4 377 tps |
| Managed vs by hand | $126.40 | EUR 39.97 | 684 vs 3 914 tps |
Prices are for the database server alone. Managed PostgreSQL also includes backups, point-in-time recovery, updates and node replacement, which are not in our column.
Summarytop
Averages of three runs per point. The multiplier is how many times ServersCamp is faster.
| Writes (TPC-B) | Reads (select-only) | Single-client commit | Database server, per month | |
|---|---|---|---|---|
| vs droplet on local disk | x2.0 to x2.5 | x1.3 to x1.9 | x6.0 | EUR 39.97 vs $48 |
| vs droplet on a Volume | x2.8 to x5.3 | x2.0 to x6.5 | x5.6 | EUR 39.97 vs $64 |
| vs Managed PostgreSQL, both over the network | x4.4 to x5.7 | x2.7 to x4.0 | x5.2 | EUR 39.97 vs $126.40 |
synchronous_commit = off, which means a confirmed transaction can be lost when the node fails. We turned it on for our measurements. The managed price also covers backups, point-in-time recovery and updates.
Conclusiontop
We are not writing a conclusion.
Reproduce ittop
Install PostgreSQL and apply the tuning on the database server.
apt-get install -y postgresql-18
RAM_MB=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo); CORES=$(nproc)
SB=$((RAM_MB/4)); EC=$((RAM_MB*3/4)); MW=$((RAM_MB/16)); [ $MW -gt 2048 ] && MW=2048
WM=$(( (RAM_MB-SB)*1024/(200*3) ))
cat > /etc/postgresql/18/main/conf.d/bench.conf <<EOF
max_connections = 200
shared_buffers = ${SB}MB
effective_cache_size = ${EC}MB
maintenance_work_mem = ${MW}MB
work_mem = ${WM}kB
wal_buffers = 16MB
min_wal_size = 2GB
max_wal_size = 16GB
checkpoint_completion_target = 0.9
random_page_cost = 1.1
effective_io_concurrency = 256
max_worker_processes = ${CORES}
max_parallel_workers = ${CORES}
max_parallel_workers_per_gather = $((CORES/2))
EOF
systemctl restart postgresql@18-main
sudo -u postgres createdb bench
Run the ladder. Add -h, -p and -U to every command when the database is on another server.
for N in 100 1000 6500; do
pgbench -i -I dtGvp -s $N bench
psql -c "CHECKPOINT;" bench; sleep 30
for r in 1 2 3; do pgbench -c 32 -j 4 -T 120 -P 10 bench; sleep 30; done
for r in 1 2 3; do pgbench --select-only -c 32 -j 4 -T 120 -P 10 bench; sleep 15; done
done
Single-client commits and the fsync test.
psql -c "CREATE TABLE fsync_t (id bigserial, v int);" bench
echo "INSERT INTO fsync_t (v) VALUES (1);" > /tmp/fsync_insert.sql
pgbench -n -c 1 -j 1 -T 60 -P 10 -f /tmp/fsync_insert.sql bench
/usr/lib/postgresql/18/bin/pg_test_fsync -f /path/on/the/data/disk/testfile -s 5
Dump and restore.
pg_dump -Fd -j 4 -f /path/on/the/data/disk/dump bench
dropdb bench && createdb bench
pg_restore -j 4 -d bench /path/on/the/data/disk/dump
On DigitalOcean Managed PostgreSQL, check the commit setting and turn it on for the test database.
psql "$MANAGED_URL" -c "SELECT name, setting, source, boot_val FROM pg_settings WHERE name = 'synchronous_commit';"
psql "$MANAGED_URL" -c "ALTER DATABASE bench SET synchronous_commit = on;"
If your numbers on a ServersCamp VM come out below what this page shows, that is a bug. Tell us in a ticket.
Appendix. CPUtop
Other numbers from the same machines. Geekbench 6 gave 623 vs 1 081 single core and 1 809 vs 3 353 multi core. sysbench CPU gave 780 vs 1 045 events per second on one thread and 2 710 vs 4 174 on four. Raw output of every run is available on request.