Test environment
PostgreSQL TDE is an open source build of PostgreSQL that encrypts data before writing it to disk, shipped as part of CYBERTEC PostgreSQL Enterprise Edition (PGEE). To quantify what that encryption costs, we ran read-only and read-write benchmarks across several database sizes.
The hardware was a AMD Ryzen 5950X with 16 cores / 32 threads, 72MB cache and AES-NI support, backed by 64GB of DDR4 2666 ECC dual-channel memory and a Samsung 980 PRO NVMe SSD on PCIe v4 ×4, presented as a simple LVM volume with an XFS filesystem for exclusive use. Software: Linux Centos 7.9.2009 on kernel 5.10.11-1.el7.elrepo.x86_64, and postgresql13-13.1-3PGDG.rhel7.x86_64 from the PGDG repo.
Two local TDE builds were produced with the standard compile options used for PGDG packages: one with the base GCC 4.8.5 20150623 (Red Hat 4.8.5-44), the other with devtoolset-9 GCC 9.3.1 20200408 (Red Hat 9.3.1-2) and -march=znver2.
Configurations and workload setup
Each measurement ran on a freshly initialized, empty cluster (initdb), so PostgreSQL-side caching effects are not a factor. Five builds were compared:
- stock — standard PostgreSQL binaries from PGDG packages.
- TDE-4 — TDE with encryption disabled, gcc 4.8 binaries.
- TDE-9 — TDE with encryption disabled, gcc 9.3 binaries optimized for znver2.
- TDE-4-e — TDE with encryption enabled, gcc 4.8 binaries.
- TDE-9-e — TDE with encryption enabled, gcc 9.3 binaries optimized for znver2.
Comparing compiler versions, not just encrypted against unencrypted, proved worthwhile — the toolchain turns out to matter considerably.
The postgresql.conf variables were derived with the CYBERTEC configurator.
max_connections = 100 unix_socket_directories = '/var/run/postgresql' port = 15432 shared_preload_libraries = 'pg_stat_statements' shared_buffers = 16GB work_mem = 64MB maintenance_work_mem = 620MB effective_cache_size = 45GB effective_io_concurrency = 100 huge_pages = try track_io_timing = on track_functions = pl wal_level = logical max_wal_senders = 10 synchronous_commit = on checkpoint_timeout = '15 min' checkpoint_completion_target = 0.9 max_wal_size = 1GB min_wal_size = 512MB wal_compression = on wal_buffers = -1 wal_writer_delay = 200ms wal_writer_flush_after = 1MB bgwriter_delay = 200ms bgwriter_lru_maxpages = 100 bgwriter_lru_multiplier = 2.0 bgwriter_flush_after = 0 max_worker_processes = 16 max_parallel_workers_per_gather = 8 max_parallel_maintenance_workers = 8 max_parallel_workers = 16 parallel_leader_participation = on enable_partitionwise_join = on enable_partitionwise_aggregate = on jit = on
Scale factors and their resulting database sizes:
- 15: 244–287MB
- 20: 332–404MB
- 25: 400–458MB
- 30: 479–543MB
- 35: 557–629MB
- 50: 793–880MB
- 500: 7849–8028MB
- 5000: 78409–78756MB (exceeds total available memory)
Each scale factor was exercised with pgbench's built-in read-only benchmark and its TPC-like read/write benchmark, at default options. Before every TPC run the database was re-indexed so that index state would not skew results. Connection counts started at 12 and stepped up through 16 (physical cores), 20, 24, 28, 32 (hardware threads), 36, 40, 44 and 48.
Read-only results
At a scale factor of 50 (880MB — a small database) the 12-connection row for standard PostgreSQL showed 598,000 read-only transactions per second. The compiler line is the interesting one: 651k TPS versus 598k TPS is a 9% gain that comes essentially for free with gcc 9.
| R-O | |||||
| 50 | |||||
| connections | stock | TDE-4 | TDE-4-e | TDE-9 | TDE-9-e |
| 12 | 598.004,94 | 652.870,65 | 640.867,94 | 651.161,67 | 668.317,19 |
| 16 | 717.080,83 | 806.152,21 | 795.300,37 | 794.494,71 | 821.915,21 |
| 20 | 735.584,04 | 813.589,44 | 801.139,14 | 807.197,44 | 829.891,65 |
| 24 | 757.834,24 | 818.350,37 | 809.893,87 | 817.332,59 | 832.810,15 |
| 28 | 865.626,22 | 911.020,73 | 896.977,27 | 909.858,10 | 916.804,18 |
| 32 | 1.095.158,47 | 1.212.918,76 | 1.175.538,12 | 1.182.331,49 | 1.233.930,94 |
| 36 | 885.519,57 | 958.732,55 | 931.091,56 | 967.514,37 | 986.631,16 |
| 40 | 847.388,93 | 943.701,45 | 915.369,45 | 919.076,23 | 964.899,29 |
| 44 | 830.018,15 | 914.696,79 | 901.860,60 | 913.789,27 | 954.813,04 |
| 48 | 818.142,23 | 916.199,48 | 894.018,17 | 901.936,11 | 964.656,08 |

PostgreSQL TDE encrypts 8k blocks on their way to disk and decrypts them when a block is fetched back from the operating system. Because of that placement, encrypted and unencrypted runs show no meaningful difference — only the expected fluctuations.

Scale factor 500 (8GB) looks the same, and again stays fully in RAM. Encryption on or off makes no real difference here either:
| R-O | |||||
| 500 | |||||
| connections | stock | TDE-4 | TDE-4-e | TDE-9 | TDE-9-e |
| 12 | 583.003,70 | 623,487,99 | 611.303,93 | 615.277,51 | 628.507,18 |
| 16 | 723.460,41 | 772.852,04 | 750.083,11 | 761.129,07 | 782.628,12 |
| 20 | 750.054,44 | 782.333,97 | 770.649,65 | 778.393,73 | 795.200,14 |
| 24 | 755.796,29 | 792.365,30 | 773.744,79 | 787.903,80 | 802.164,80 |
| 28 | 842.270,11 | 890.584,06 | 872.163,41 | 886.265,42 | 895.721,71 |
| 32 | 1.087.753,87 | 1.166.292,93 | 1.124.810,04 | 1.133.026,80 | 1.170.395,10 |
| 36 | 881.376,33 | 926.521,14 | 901.508,03 | 914.466,29 | 948.293,12 |
| 40 | 860.762,15 | 911.308,67 | 875.478,96 | 911.961,05 | 912.369,46 |
| 44 | 857.354,28 | 895.126,03 | 863.349,07 | 878.027,97 | 916.855,40 |
| 48 | 853.062,65 | 881.192,84 | 858.566,69 | 882.463,09 | 910.616,50 |

Scale factor 5000 pushes the dataset beyond what RAM can hold, and performance drops sharply. Disk access costs many times more than pointer arithmetic in shared memory; the added latency per query depresses TPS when there are too few connections. More concurrency offsets this somewhat on SSD, but only until disk capacity is saturated.
| R-O | |||||
| 5000 | |||||
| connections | stock | TDE-4 | TDE-4-e | TDE-9 | TDE-9-e |
| 12 | 78.858,92 | 75.226,39 | 74.968,65 | 80.913,53 | 82.063,75 |
| 16 | 104.880,27 | 99.974,75 | 99.921,82 | 106.414,78 | 107.763,35 |
| 20 | 126.328,88 | 120.898,40 | 119.139,41 | 124.199,94 | 125.980,44 |
| 24 | 143.740,06 | 139.041,17 | 134.917,30 | 136.937,95 | 140.407,50 |
| 28 | 156.813,58 | 152.443,32 | 147.754,62 | 148.286,04 | 152.065,41 |
| 32 | 170.999,35 | 162.929,84 | 156.527,94 | 157.810,55 | 164.181,20 |
| 36 | 180.937,04 | 171.578,25 | 165.513,33 | 167.660,63 | 172.848,70 |
| 40 | 189.501,24 | 180.439,46 | 173.084,98 | 174.142,58 | 179.936,11 |
| 44 | 196.615,29 | 187.607,67 | 178.765,27 | 179.044,87 | 185.212,72 |
| 48 | 201.491,30 | 192.716,50 | 183.124,81 | 183.379,49 | 190.149,00 |

Read-write results
Read-write performance is substantially lower overall. A pgbench read-write transaction issues far more commands, and PostgreSQL must flush (fsync) changes to disk on commit.
| TPC | |||||
| 50 | |||||
| connections | stock | TDE-4 | TDE-4-e | TDE-9 | TDE-9-e |
| 12 | 3.580,13 | 3.520,69 | 2.014,56 | 2.014,16 | 2.024,19 |
| 16 | 4.537,57 | 4.493,65 | 2.548,15 | 2.565,76 | 2.547,17 |
| 20 | 5.311,45 | 5.374,37 | 3.002,80 | 5.091,04 | 2.995,52 |
| 24 | 6.192,00 | 6.208,62 | 3.432,04 | 4.998,44 | 3.420,59 |
| 28 | 6.904,77 | 6.766,71 | 3.784,67 | 4.923,47 | 3.803,69 |
| 32 | 7.626,48 | 7.473,48 | 4.134,59 | 4.725,00 | 4.139,35 |
| 36 | 8.040,79 | 7.838,61 | 4.456,76 | 5.474,01 | 4.463,35 |
| 40 | 8.389,51 | 8.408,85 | 4.697,79 | 7.043,24 | 4.713,90 |
| 44 | 8.971,14 | 8.853,69 | 4.983,57 | 4.982,57 | 4.965,51 |
| 48 | 9.246,47 | 9.085,38 | 5.191,75 | 7.751,70 | 5.170,42 |
Here the cost of encryption begins to show: encrypted variants can be significantly slower than the non-encrypted configuration.

At scale factor 500 the picture is similar:
| TPC | |||||
| 500 | |||||
| connections | stock | TDE-4 | TDE-4-e | TDE-9 | TDE-9-e |
| 12 | 4.088,59 | 4.098,60 | 2.201,37 | 3.106,54 | 3.080,33 |
| 16 | 5.417,76 | 5.372,61 | 2.818,17 | 3.400,99 | 3.278,76 |
| 20 | 6.716,89 | 6.688,04 | 3.437,78 | 3.712,33 | 3.672,65 |
| 24 | 7.792,77 | 7.903,35 | 4.021,37 | 4.484,28 | 4.576,07 |
| 28 | 9.061,49 | 7.850,11 | 4.631,35 | 4.926,10 | 5.150,77 |
| 32 | 10.399,70 | 10.428,75 | 5.235,09 | 5.515,01 | 5.818,24 |
| 36 | 11.631,82 | 11.737,26 | 5.811,99 | 6.056,26 | 6.381,50 |
| 40 | 12.998,91 | 12.986,14 | 6.349,06 | 6.911,43 | 6.526,33 |
| 44 | 14.201,98 | 14.301,64 | 6.913,72 | 7.360,12 | 7.472,31 |
| 48 | 14.908,41 | 15.259,67 | 7.489,46 | 8.097,42 | 7.650,18 |

At scale factor 5000 overall throughput falls dramatically because much of the data must come from disk, producing substantial disk wait and latency:
| TPC | |||||
| 5000 | |||||
| connections | stock | TDE-4 | TDE-4-e | TDE-9 | TDE-9-e |
| 12 | 4.232,58 | 4.350,82 | 2.176,31 | 2.183,95 | 2.167,18 |
| 16 | 5.466,42 | 5.259,30 | 2.908,34 | 2.960,76 | 2.908,90 |
| 20 | 4.791,08 | 6.854,99 | 3.385,01 | 3.415,35 | 3.381,12 |
| 24 | 4.676,96 | 5.967,64 | 3.983,86 | 3.993,32 | 3.994,85 |
| 28 | 4.568,79 | 4.544,86 | 4.514,53 | 4.547,60 | 4.537,21 |
| 32 | 5.151,62 | 5.085,77 | 5.058,35 | 5.087,52 | 5.029,79 |
| 36 | 5.658,66 | 5.718,43 | 5.507,31 | 5.616,75 | 5.511,39 |
| 40 | 6.138,71 | 6.167,15 | 5.952,18 | 5.948,25 | 6.007,10 |
| 44 | 6.437,95 | 6.569,21 | 6.453,01 | 6.356,46 | 6.432,60 |
| 48 | 6.968,81 | 7.012,23 | 6.807,95 | 6.845,92 | 6.791,73 |

Caveats
All of these numbers should be taken with a grain of salt — run-times vary, particularly on SSDs, as we have seen repeatedly. Expected performance also depends heavily on PostgreSQL cache behavior, because encryption and decryption happen every time a block moves between shared buffers and disk. It can therefore make sense to run PostgreSQL TDE with higher shared_buffers settings than usual, to limit the impact of security.
A follow-up round of these analyses is in preparation, with proper TRIM management via the -o discard mount option and scales chosen to better isolate the effect of dataset size against available memory: 12 (an order of magnitude below shared buffers), 900 (90% of shared buffers), 1600 (120% of shared buffers, still fitting in RAM) and 5000 (120% of RAM). Other parameters and tooling stay the same.


