A technical, production-grade benchmarking suite evaluating PostgreSQL as a high-throughput, low-latency Key-Value cache against Valkey (Redis open-source fork) and an in-memory Go cache engine (Otter).
In modern cloud architectures, introducing a dedicated key-value store (such as Redis or Valkey) is the standard pattern for application caching. While effective, this approach introduces operational complexity: managing additional cluster nodes, handling cross-network latency, maintaining data synchronization, and managing additional infrastructure costs.
This project empirically investigates a critical architectural question:
Can a properly tuned PostgreSQL instance serve as an enterprise-grade, high-throughput key-value cache while preserving data locality and simplifying infrastructure?
To answer this, we measure throughput (MB/s), request execution rates (RPS), and latency distribution under sustained concurrency across four distinct caching paradigms over an endurance testing protocol.
The benchmark runner executes an ultra-low-allocation HTTP service written in Go (fasthttp), exposing standardized GET/SET endpoints across five storage backends:
+----------------------------------+
| Vegeta Load Generator |
+----------------------------------+
|
v
+----------------------------------+
| Go fasthttp Engine (Port 8080) |
| (Zero-Alloc Buffer Recycling) |
+----------------------------------+
|
+------------+----------+---------+---------+-------------------+
| | | | |
v v v v v
+----------+ +---------+ +--------------------+ +--------------------+ +--------------------+
| Otter | | Valkey | | Standard Postgres | | Optimized Postgres | | Postgres TSID |
| (Memory) | | (9.1) | |(Relational/UUIDv7) | |(UNLOGGED/VTProto) | | (64-bit TSID Key) |
+----------+ +---------+ +--------------------+ +--------------------+ +--------------------+
-
In-Memory (
Otter)- Mechanism: Native Go in-memory cache leveraging
github.com/maypok86/otter/v2with automated lock-free TTL expiration. - Role: Serves as the theoretical performance ceiling (zero network, zero serialization overhead).
- Mechanism: Native Go in-memory cache leveraging
-
Valkey Key-Value Store
- Mechanism: Valkey 9.1 standalone instance connected via a tuned TCP connection pool (2,500 connections).
- Payload Format: Protobuf binary wire format compiled via
planetscale/vtprotobuf(MarshalVT/UnmarshalVT). - Indexing: 26-character Base32 Crockford ULIDs backed by
github.com/oklog/ulid/v2.
-
Standard PostgreSQL (
Relational / Flat)- Mechanism: Standard relational table (
users_standard) with individual columns per field, pre-populated with 10,000,000 background rows (~1.3 GB of data) to evaluate query latency against a production-scale relational table. - Queries: Prepared statements executed at the connection layer (
pgxpool) to eliminate SQL parsing overhead. - Indexing: Binary UUID primary key (
UUID/ 16 bytes).
- Mechanism: Standard relational table (
-
Optimized PostgreSQL (
UNLOGGED / Protobuf VTProto)- Mechanism: Dedicated key-value cache architecture (
cache_optimizedunlogged table). Designed for transient workloads, it operates as a dedicated cache store holding the active working set (100,000 seeded keys). - Payload Format: Protobuf VTProto zero-allocation byte payload stored in a compact
byteacolumn. - Kernel Optimizations: Disables Write-Ahead Logging (
UNLOGGED), reserves page space for HOT updates (fillfactor = 70), and executes non-blocking background cleanup (FOR UPDATE SKIP LOCKED).
- Mechanism: Dedicated key-value cache architecture (
-
PostgreSQL TSID (
Relational / 64-bit TSID)- Mechanism: Relational table schema (
users_tsid) leveraging 64-bit Time-Sorted Identifiers (TSID) stored as native 8-bytebigintprimary keys, pre-populated with 10,000,000 background rows (~1.3 GB of data) for compact B-Tree index traversal.
- Mechanism: Relational table schema (
Instead of Go's reflection-heavy standard proto.Marshal or json.Marshal, all structured payloads are serialized using PlanetScale's vtprotobuf generator:
- Zero Heap Allocations: Encodes (
MarshalVT) and decodes (UnmarshalVT) binary payloads without runtime reflection or dynamic struct allocations. - Wire Format Compatibility: Fully compatible with standard Protobuf v3 specifications.
PostgreSQL is engineered by default for strict ACID data durability. To maximize throughput and minimize latency for caching workloads, RAM allocations, Write-Ahead Logging (WAL), and page management must be tuned (postgresql.conf or ALTER SYSTEM SET ...):
shared_buffers = 25%of Total System RAM (e.g., 1GB on a 4GB system, 4GB on 16GB RAM): Primary shared memory cache where PostgreSQL buffers table pages and B-Tree index blocks read from disk.work_mem = 16MBto64MB: Memory allocated for sorting operations (ORDER BY,DISTINCT,JOIN). Allocated per query operation per connection; tuned to prevent Linux Kernel OOM invocation under high concurrent connection pools.effective_cache_size = 50%to75%of Total System RAM: Estimate provided to the query planner representing available RAM (PostgreSQL shared buffers + Linux OS page cache), encouraging high-performance B-Tree index scans.maintenance_work_mem = 256MBto1GB: Accelerates index creation (CREATE INDEX), table maintenance (VACUUM), and bulk foreign key validations.
max_wal_size = 4GBto16GB(increased from default 1GB): Expands WAL log capacity before triggering forced checkpoints, eliminating disk I/O contention during sustained write spikes.checkpoint_completion_target = 0.9: Spreads checkpoint disk writes over 90% of the checkpoint duration window, preventing latency spikes.wal_buffers = 16MB: Memory buffer allocation for unwritten WAL data prior to disk flushing.
UNLOGGEDTables: Disables Write-Ahead Logging (WAL) for transient cache items, achieving near-in-memory write speeds by eliminating disk I/O synchronization.- HOT (Heap-Only Tuple) Optimization (
fillfactor = 70): Reserves 30% page space on table blocks to allow in-place tuple updates, eliminating B-Tree index rewrite overhead during frequent key overwrites. - Non-Blocking Asynchronous Purge (
FOR UPDATE SKIP LOCKED): Expired cache items are purged in background batches via PL/pgSQL using non-blocking row locks, preventingGETrequests from blocking during garbage collection. - Compact Keys: 16-byte binary UUID v7 / ULID and 8-byte TSID keys keep index node sizes minimal, ensuring B-Tree structures fit inside
shared_buffers.
Valkey/Redis executes on a single primary event-loop thread for core data manipulations. Performance bottlenecks stem from single-core CPU execution and network I/O bandwidth. The following configurations optimize Valkey as an in-memory cache:
maxmemory = 75%of Server RAM (e.g.,maxmemory 3gbon a 4GB server): Sets a strict memory limit to prevent Linux Kernel OOM killer termination.maxmemory-policy = allkeys-lruorvolatile-lru: Automatically evicts Least Recently Used (LRU) keys when memory limit is reached to accommodate new cache insertions.
io-threads = 2or4: Offloads RESP protocol parsing and socket write operations to secondary worker threads (e.g., 2-3 threads on a 4-vCPU system), freeing the primary thread exclusively for in-memory key execution.io-threads-do-reads = yes: Enables multi-threaded network reading and payload parsing for incoming request sockets.
When operating exclusively as a transient application cache:
save "": Disables background RDB disk snapshots.appendonly no: Disables Append-Only File (AOF) logging to save CPU cycles and eliminate disk I/O latency.
fasthttpCore: Replaces standardnet/httpwith high-performance byte-slice parsing and connection buffer recycling.- Struct Pooling: Recycles
model.UserDatadomain entities viasync.Poolduring deserialization, eliminating Garbage Collector overhead under heavy GET loads. - Standardized Identifier Libraries: Leverages
github.com/google/uuidfor monotonic UUID v7 generation,github.com/oklog/ulid/v2for 26-character Base32 ULIDs, and 64-bit TSIDs (bigint).
To ensure reproducible and un-biased metrics, tests adhere to a strict isolation protocol:
-
Hardware & Process Isolation:
- Go Server: Bound to 4 CPU cores (
GOMAXPROCS=4) and 10GiB memory limit (GOMEMLIMIT=10GiB). - PostgreSQL: Constrained via
systemdcgroups (CPUQuota=200%,MemoryMax=4G). - Valkey: Constrained via
systemdcgroups (CPUQuota=200%,MemoryMax=4G). Even if Valkey is mono-threaded, it benefits from dedicated resources to handle the I/O load.
- Go Server: Bound to 4 CPU cores (
-
OS Network Socket Tuning:
- Automated TCP socket recycling (
net.ipv4.tcp_tw_reuse=1) and ephemeral port range expansion (1024-65535) to preventTIME_WAITsocket exhaustion under heavy load.
- Automated TCP socket recycling (
-
Execution Sequence:
- Dataset Population: Pre-populates 100,000 unique keys (
gen/keys.txt). - Warmup Phase: 30-second target warmup to ensure cache pages and buffer pools are hot before measurement starts.
- Endurance Phase: 10-minute continuous sustained load test using Vegeta (
-rate=0,-workers=250).
- Dataset Population: Pre-populates 100,000 unique keys (
- Go 1.26+
- PostgreSQL 18+
- Valkey 9.1+ (or Redis)
- Vegeta (
go install github.com/tsenart/vegeta/v12@latest) - protoc &
protoc-gen-go-vtproto(optional, for regenerating protobuf code)
Warning
System Configuration Modification Warning:
Executing make tune-os requires elevated privileges (sudo) and permanently modifies system-level kernel parameters and service cgroups on the host operating system:
- Kernel
sysctlNetwork Tuning: Modifies TCP socket recycling (net.ipv4.tcp_tw_reuse=1), expands local ephemeral port ranges (1024-65535), increases connection backlogs (net.core.somaxconn=65535,tcp_max_syn_backlog=65535), and reduces FIN timeout (tcp_fin_timeout=15). - Systemd Service Cgroup Limits: Sets strict CPU quotas (
CPUQuota=200%) and memory limits (MemoryMax=4G) directly on system services (postgresqlandvalkey).
# Apply Linux kernel socket and systemd cgroup tuning parameters
make tune-os# Compile server binary (bin/server)
make build
# Generate 100,000 ULID test keys in gen/keys.txt
make gen-targets
# (Optional) Regenerate Protobuf VTProto code
make proto-genThe Makefile provides three execution durations:
# Quick validation smoke test (5 seconds per engine)
make quick
# Medium benchmark run (300 seconds per engine)
make medium
# Official endurance test (600 seconds / 10 minutes per engine)
make longTo run a specific engine manually for profiling or debugging:
# Launch server for a specific storage engine
./bin/server -engine optimized-postgresql -log-level production
# Execute Vegeta load test in a separate terminal
(while cat gen/keys.txt; do :; done) | \
sed 's|^|GET http://127.0.0.1:8080/postgres/get?id=|' | \
vegeta attack -lazy -rate=0 -workers=250 -duration=60s | \
vegeta report| Engine | GET Endpoint | POST Endpoint |
|---|---|---|
| In-Memory | GET /memory/get?id=<KEY> |
POST /memory/set?id=<KEY> |
| Valkey | GET /valkey/get?id=<KEY> |
POST /valkey/set?id=<KEY> |
| Standard Postgres | GET /postgres/get?id=<KEY> |
POST /postgres/set?id=<KEY> |
| Optimized Postgres | GET /postgres/get?id=<KEY> |
POST /postgres/set?id=<KEY> |
| Postgres TSID | GET /postgres/get?id=<KEY> |
POST /postgres/set?id=<KEY> |
Important
Dataset Footprint & Benchmark Isolation Context:
- Relational Baselines (
standard-postgresql&postgres-tsid): Evaluated against relational tables pre-populated with 10,000,000 background rows (~1.3 GB of data) to measure B-Tree index traversal, memory page cache behavior, and lookup latency against a large enterprise relational table footprint. - Dedicated Cache Engines (
memory,valkey,optimized-postgresql): Evaluated as dedicated key-value cache stores initialized with the active 100,000 key working set.
Evaluates read throughput and latency distribution under heavy concurrent load (250 workers).
| Engine | Storage Paradigm | Dataset Size | Total Requests | Throughput (RPS) | Mean Latency | p50 Latency | p90 Latency | p95 Latency | p99 Latency | Max Latency | Success |
|---|---|---|---|---|---|---|---|---|---|---|---|
| In-Memory (Otter) | Native Go Cache | 100k keys | 63,995,364 | 106,658.98 req/s | 1.959ms | 1.482ms | 4.415ms | 5.287ms | 7.687ms | 26.758ms | 100.00% |
| Valkey 9.1 | Standalone KV Store | 100k keys | 46,273,423 | 77,122.39 req/s | 2.955ms | 2.800ms | 4.749ms | 5.396ms | 6.782ms | 30.598ms | 100.00% |
| Postgres TSID | Relational DB (64-bit TSID) | 10M rows (~1.3 GB) | 32,395,151 | 53,991.93 req/s | 4.321ms | 4.202ms | 6.242ms | 7.096ms | 9.236ms | 33.767ms | 100.00% |
| Optimized Postgres | UNLOGGED Cache Table | 100k keys | 32,249,699 | 53,749.43 req/s | 4.338ms | 4.226ms | 6.274ms | 7.133ms | 9.237ms | 47.219ms | 100.00% |
| Standard Postgres | Relational DB (UUID v7) | 10M rows (~1.3 GB) | 31,991,527 | 53,319.26 req/s | 4.382ms | 4.317ms | 6.226ms | 6.974ms | 8.585ms | 39.832ms | 100.00% |
Measures key insertion throughput and write latency required to populate 100k keys.
| Engine | Total Requests | Rate (RPS) | Duration | Mean Latency | p50 Latency | p90 Latency | p95 Latency | p99 Latency | Max Latency | Success |
|---|---|---|---|---|---|---|---|---|---|---|
| In-Memory (Otter) | 100,011 | 94,029.10 req/s | 1.065s | 2.124ms | 1.667ms | 4.617ms | 5.627ms | 8.420ms | 22.864ms | 99.99% |
| Valkey 9.1 | 100,122 | 68,980.18 req/s | 1.452s | 3.289ms | 3.025ms | 5.082ms | 5.822ms | 7.840ms | 52.463ms | 99.88% |
| Optimized Postgres | 100,013 | 29,405.72 req/s | 3.406s | 8.415ms | 6.384ms | 8.152ms | 30.372ms | 32.468ms | 35.232ms | 99.99% |
| Standard Postgres | 100,001 | 2,754.07 req/s | 36.397s | 90.831ms | 89.224ms | 93.973ms | 95.787ms | 173.848ms | 727.267ms | 100.00% |
| Postgres TSID | 100,003 | 2,672.79 req/s | 37.506s | 93.600ms | 89.796ms | 95.459ms | 101.944ms | 253.804ms | 938.118ms | 100.00% |
-
Read Scale Efficiency on a 10M Row (~1.3 GB) Dataset:
- Even when querying relational tables scaled to 10,000,000 rows (~1.3 GB) (
standard-postgresqlandpostgres-tsid), PostgreSQL maintains ~53,300 - 54,000 req/s with a p50 latency of 4.2ms. - PostgreSQL achieves 70% of dedicated Valkey/Redis read throughput (54k vs 77k RPS) under heavy concurrent load, demonstrating that PostgreSQL B-Tree index traversal remains fast even at scale.
- Even when querying relational tables scaled to 10,000,000 rows (~1.3 GB) (
-
10.6x Write Acceleration via UNLOGGED Tables & Protobuf:
- Optimized PostgreSQL (
UNLOGGED+fillfactor=70+ Protobuf VTProto bytea payload) achieves 29,405.72 req/s on SET operations (3.4s duration for 100k keys), compared to 2,754.07 req/s for Standard PostgreSQL (36.4s duration). - Eliminating WAL disk synchronization overhead yields a 10.6x performance gain for cache write operations.
- Optimized PostgreSQL (
🔍 Click to view Raw Cleaned Benchmark Output Log
========================================================
CLEANED BENCHMARK RESULTS
========================================================
========================================================
ENGINE: memory
========================================================
Successfully generated 100000 raw UUID v7 keys in gen/keys.txt
--- Executing SET benchmark (Seeding 100k keys)... ---
Requests [total, rate, throughput] 100011, 94029.10, 93917.43
Duration [total, attack, wait] 1.065s, 1.064s, 1.148ms
Latencies [min, mean, 50, 90, 95, 99, max] 120ns, 2.124ms, 1.667ms, 4.617ms, 5.627ms, 8.42ms, 22.864ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 99.99%
Status Codes [code:count] 0:11 200:100000
--- Executing GET benchmark with Vegeta (600s)... ---
Requests [total, rate, throughput] 63995364, 106658.98, 106658.43
Duration [total, attack, wait] 10m0s, 10m0s, 3.07ms
Latencies [min, mean, 50, 90, 95, 99, max] 31.86µs, 1.959ms, 1.482ms, 4.415ms, 5.287ms, 7.687ms, 26.758ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:63995364
========================================================
ENGINE: valkey
========================================================
Successfully generated 100000 raw UUID v7 keys in gen/keys.txt
--- Executing SET benchmark (Seeding 100k keys)... ---
Requests [total, rate, throughput] 100122, 68980.18, 68871.16
Duration [total, attack, wait] 1.452s, 1.451s, 526.102µs
Latencies [min, mean, 50, 90, 95, 99, max] 110ns, 3.289ms, 3.025ms, 5.082ms, 5.822ms, 7.84ms, 52.463ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 99.88%
Status Codes [code:count] 0:122 200:100000
--- Executing GET benchmark with Vegeta (600s)... ---
Requests [total, rate, throughput] 46273423, 77122.39, 77122.12
Duration [total, attack, wait] 10m0s, 10m0s, 2.077ms
Latencies [min, mean, 50, 90, 95, 99, max] 69.701µs, 2.955ms, 2.8ms, 4.749ms, 5.396ms, 6.782ms, 30.598ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:46273423
========================================================
ENGINE: standard-postgresql
========================================================
Successfully generated 100000 raw UUID v7 keys in gen/keys.txt
--- Executing SET benchmark (Seeding 100k keys)... ---
Requests [total, rate, throughput] 100001, 2754.07, 2747.51
Duration [total, attack, wait] 36.397s, 36.31s, 86.251ms
Latencies [min, mean, 50, 90, 95, 99, max] 882ns, 90.831ms, 89.224ms, 93.973ms, 95.787ms, 173.848ms, 727.267ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:1 200:100000
--- Executing GET benchmark with Vegeta (600s)... ---
Requests [total, rate, throughput] 31991527, 53319.26, 53318.95
Duration [total, attack, wait] 10m0s, 10m0s, 3.43ms
Latencies [min, mean, 50, 90, 95, 99, max] 102.814µs, 4.382ms, 4.317ms, 6.226ms, 6.974ms, 8.585ms, 39.832ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:31991527
========================================================
ENGINE: optimized-postgresql
========================================================
Successfully generated 100000 raw UUID v7 keys in gen/keys.txt
--- Executing SET benchmark (Seeding 100k keys)... ---
Requests [total, rate, throughput] 100013, 29405.72, 29361.66
Duration [total, attack, wait] 3.406s, 3.401s, 4.661ms
Latencies [min, mean, 50, 90, 95, 99, max] 340ns, 8.415ms, 6.384ms, 8.152ms, 30.372ms, 32.468ms, 35.232ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 99.99%
Status Codes [code:count] 0:13 200:100000
--- Executing GET benchmark with Vegeta (600s)... ---
Requests [total, rate, throughput] 32249699, 53749.43, 53749.21
Duration [total, attack, wait] 10m0s, 10m0s, 2.444ms
Latencies [min, mean, 50, 90, 95, 99, max] 81.534µs, 4.338ms, 4.226ms, 6.274ms, 7.133ms, 9.237ms, 47.219ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:32249699
========================================================
ENGINE: postgres-tsid
========================================================
Successfully generated 100000 raw TSID 64-bit keys in gen/keys.txt
--- Executing SET benchmark (Seeding 100k keys)... ---
Requests [total, rate, throughput] 100003, 2672.79, 2666.21
Duration [total, attack, wait] 37.506s, 37.415s, 91.308ms
Latencies [min, mean, 50, 90, 95, 99, max] 401ns, 93.6ms, 89.796ms, 95.459ms, 101.944ms, 253.804ms, 938.118ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 0:3 200:100000
--- Executing GET benchmark with Vegeta (600s)... ---
Requests [total, rate, throughput] 32395151, 53991.93, 53991.62
Duration [total, attack, wait] 10m0s, 10m0s, 3.404ms
Latencies [min, mean, 50, 90, 95, 99, max] 105.509µs, 4.321ms, 4.202ms, 6.242ms, 7.096ms, 9.236ms, 33.767ms
Bytes In [total, mean] 0, 0.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:32395151
When choosing between a dedicated in-memory key-value cache (Valkey/Redis) and leveraging PostgreSQL as a high-performance cache store, architects must evaluate six critical considerations:
- Single-Database Architecture (PostgreSQL Only):
- Pros: Simplifies system topology, eliminates cache invalidation race conditions, reduces Cloud infrastructure cost (no extra Redis nodes/clusters), unifies backup and point-in-time recovery (PITR).
- Cons: Shares CPU/RAM resources between relational queries and caching workloads.
- Dual-Database Architecture (PostgreSQL + Valkey/Redis):
- Pros: Isolate cache workloads to dedicated memory-optimized nodes; sub-millisecond latencies under ultra-high RPS (>75k+ req/s).
- Cons: Introduces cross-network RPC latency, cache synchronization complexity, dual-write consistency hazards, and additional node management overhead.
- RAM-Bound Stores (Valkey / Otter):
- Store all keys and values strictly in volatile RAM. When capacity (
maxmemory) is exceeded, data must be evicted using policies likeallkeys-lruor writes will fail.
- Store all keys and values strictly in volatile RAM. When capacity (
- Disk-Backed RAM Cache (PostgreSQL):
- PostgreSQL uses a hybrid memory model: active pages and index nodes sit in
shared_buffersand Linux OS page cache. If RAM is exceeded, cold pages are transparently paged to disk instead of triggering memory eviction or OOM crashes.
- PostgreSQL uses a hybrid memory model: active pages and index nodes sit in
- ACID Logged Storage (
standard-postgresql/postgres-tsid):- Guarantees 100% crash safety via Write-Ahead Logging (WAL). Every mutation (
SET) waits for synchronous disk journal flushes, capping write throughput at ~2,750 req/s.
- Guarantees 100% crash safety via Write-Ahead Logging (WAL). Every mutation (
- Transient
UNLOGGEDStorage (optimized-postgresql):- Bypasses WAL logging entirely, raising write throughput by 10.6x to 29,400 req/s. If the database node crashes,
UNLOGGEDtables are truncated on restart—a perfect trade-off for cache tables where missing keys can be re-hydrated from source tables.
- Bypasses WAL logging entirely, raising write throughput by 10.6x to 29,400 req/s. If the database node crashes,
- 16-Byte Monotonic Identifiers (UUID v7 / ULID):
- Sequential timestamp prefixes ensure B-Tree insertions happen at the rightmost page edge, preventing page splitting and index bloat.
- 8-Byte Compact Identifiers (TSID
bigint):- Halves primary key storage (8 bytes vs 16 bytes). Smaller index nodes allow 2x more key entries per B-Tree page block, maximizing CPU L1/L2 cache hit ratios and optimizing
shared_buffersusage across 10,000,000+ rows.
- Halves primary key storage (8 bytes vs 16 bytes). Smaller index nodes allow 2x more key entries per B-Tree page block, maximizing CPU L1/L2 cache hit ratios and optimizing
- Protobuf VTProto Binary Payloads:
- Serializes domain objects into zero-allocation byte arrays stored in a PostgreSQL
byteacolumn. Eliminates multi-column SQL parsing and runtime Go reflection.
- Serializes domain objects into zero-allocation byte arrays stored in a PostgreSQL
- Relational Field Mapping:
- Maps individual struct fields to separate database columns. Provides SQL queryability (
WHERE age > 30) at the cost of higher query parsing and tuple assembly overhead.
- Maps individual struct fields to separate database columns. Provides SQL queryability (
Based on empirical benchmark data, engineering complexity, energy efficiency, and operational SLAs, we formulate specific recommendations across four distinct caching paradigms (including the choice to eliminate caching altogether):
- Core Profile: Embedded Go memory cache executing in-process without network RPCs.
- Performance: Maximum throughput (106.6k GET RPS / 94.0k SET RPS) and lowest latency (1.48ms p50).
- Energy & Hardware Sobriety: Maximum. Zero network TCP socket allocations, zero protocol serialization, and zero dedicated idle infrastructure.
- Limitations: Bound to single-node application memory; cannot be shared across horizontally scaled microservice replicas without external synchronization.
- Recommendation: Ideal for L1 micro-caching, immutable metadata, static application configurations, or single-process hot-path lookups.
- Core Profile: Dedicated standalone/clustered key-value cache running on isolated memory-optimized instances.
- Performance: Excellent throughput (77.1k GET RPS / 69.0k SET RPS) and low latency (2.80ms p50).
- Energy & Hardware Sobriety: Medium. Requires dedicated cloud servers/clusters running continuous event loops, introducing network serialization (RESP protocol) overhead and idle energy footprints.
- Limitations: Introduces operational complexity, cross-network RPC hops, cluster failover management, and risk of cache invalidation bugs (stale cache vs. primary DB).
- Recommendation: Essential for multi-instance shared session stores, distributed rate-limiting, real-time pub/sub, or applications requiring sustained >75,000+ GET req/s.
- Core Profile: Key-Value cache stored inside PostgreSQL using an
UNLOGGEDtable (fillfactor = 70) and binary Protobuf VTProto payloads. - Performance: Strong read throughput (53.7k GET RPS) and 10.6x faster write throughput (29.4k SET RPS) compared to standard SQL transactions.
- Energy & Hardware Sobriety: High. Reuses existing database infrastructure, eliminating the energy footprint and cost of extra Redis/Valkey cluster instances and avoiding additional network hops when co-located.
- Limitations:
UNLOGGEDtables are truncated upon PostgreSQL hard crashes/reboots (ephemeral cache behavior). Missing keys must be re-hydrated from source tables. - Recommendation: Ideal for teams seeking to simplify infrastructure to a Single Database Stack while requiring 10x-faster cache write mutations (29.4k RPS) alongside high read throughput (53.7k RPS).
🟢 Typology D: Direct Query on Primary Relational Database / "No Cache Layer" (standard-postgresql / postgres-tsid)
- Core Profile: Querying the primary relational database tables directly, evaluated against 10,000,000 background rows (~1.3 GB).
- Performance: Exceptional read scale (53.3k - 54.0k GET RPS @ 4.2ms p50 latency). Write throughput is bound by WAL journal flushes (~2,700 SET RPS).
- Energy & Hardware Sobriety: Maximum. Eliminates data duplication, eliminates cache invalidation code, and avoids redundant dual-write CPU/network cycles.
- Limitations: Write throughput is capped by WAL disk I/O; heavy analytical writes can contend with read lookups.
- Recommendation: Recommended as the default baseline architecture for read-heavy applications (up to 50,000+ req/s) with write rates below ~2,500 req/s. Proves that a properly indexed 10M-row PostgreSQL table is often fast enough, avoiding premature optimization and extra infrastructure.
| Evaluation Dimension | Typology A: Local In-Memory (Otter) |
Typology B: Distributed Store (Valkey 9.1) |
Typology C: Postgres UNLOGGED Cache (optimized-postgresql) |
Typology D: Direct DB / No Cache (standard-postgresql / postgres-tsid) |
|---|---|---|---|---|
| GET Read Throughput (RPS) | 106,658 req/s 🚀 | 77,122 req/s 🟢 | 53,749 req/s 🟢 | 53,319 - 53,991 req/s 🟢 |
| SET Write Throughput (RPS) | 94,029 req/s 🚀 | 68,980 req/s 🟢 | 29,405 req/s 🟡 | 2,672 - 2,754 req/s 🔴 |
| p50 Read Latency | 1.48 ms | 2.80 ms | 4.22 ms | 4.20 - 4.31 ms |
| p99 Read Latency | 7.69 ms | 6.78 ms | 9.24 ms | 8.59 - 9.24 ms |
| Operational Complexity | Low (Infra) / Medium (App) (Requires app code to correlate cache state & sync with primary DB) | High (Cluster deployment, failover, Redis proxy) | Low (Reuses Postgres pool & schema) | Minimum (Single DB stack, single source of truth) |
| Energy & Hardware Efficiency | Maximum (Zero network I/O, zero idle server power) | Medium (Dedicated idle VMs/clusters, network serialization) | High (Reuses DB hardware, no extra network hops) | Maximum (Zero data duplication, zero dual-write CPU usage) |
| Consistency Hazards | N/A (Process-local) | High (Stale cache vs DB, invalidation race conditions) | Medium (Table-level TTL / background PL/pgSQL purge) | Zero Risk (100% ACID consistency guaranteed) |
| Durability / Crash Recovery | Volatile (Lost on process exit) | Ephemeral / Configurable (RDB/AOF) | Ephemeral (Truncated on DB hard restart) | 100% ACID Guaranteed (Full WAL journal safety) |
| Memory Overflow Behavior | App RAM bound | RAM bound (LRU eviction) | Hybrid (RAM shared_buffers + transparent disk overflow) |
Hybrid (RAM shared_buffers + transparent disk overflow) |
| Architectural Verdict | Best for L1 local cache & static config | Best for shared sessions & >75k+ RPS SLAs | Best for high-volume cache on single DB stack | Best default for <2.5k write RPS (Prevents over-engineering) |
Note
Empirical Key Takeaway: Standardize on Native UUID v7
Although TSID (bigint 64-bit, 8 bytes) theoretically halves primary key storage compared to UUID v7 (128-bit, 16 bytes), empirical benchmark measurements on a 10,000,000-row (~1.3 GB) dataset show a negligible performance difference (< 1.2% variance):
- Read Throughput: 53,991 req/s (TSID) vs. 53,319 req/s (UUID v7), with p50 latencies virtually identical (4.20 ms vs. 4.31 ms).
- Write Throughput: 2,672 req/s (TSID) vs. 2,754 req/s (UUID v7).
Why UUID v7 is the Recommended Choice:
- Native PostgreSQL & Tooling Support: PostgreSQL features a native
uuidcolumn type. Standardizing on UUID v7 (RFC 9562) leverages built-in driver, ORM, and database ecosystem support without custom 64-bit ID generator orchestration. - Collision Safety at Scale: With 128 bits of space (including 74 bits of entropy), UUID v7 guarantees global uniqueness across distributed microservices and multi-region deployments without requiring central worker ID allocation.
- Time-Ordered B-Tree Efficiency: Like TSID, UUID v7 embeds a monotonic timestamp prefix, ensuring sequential rightmost insertions into B-Tree index pages and completely preventing random-insert page splitting.
Conclusion: Because TSID's compact 8-byte footprint yields no tangible throughput or latency advantage in PostgreSQL, UUID v7 is the superior architectural choice due to its native ergonomics, ecosystem integration, and robust distributed collision safety.
This repository is distributed under the MIT License. See LICENSE for details.