惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
Cloudbric
Cloudbric
I
InfoQ
V
V2EX
博客园_首页
The Register - Security
The Register - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
Secure Thoughts
Vercel News
Vercel News
Forbes - Security
Forbes - Security
云风的 BLOG
云风的 BLOG
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
D
DataBreaches.Net
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
B
Blog RSS Feed
A
About on SuperTechFans
N
News and Events Feed by Topic
Apple Machine Learning Research
Apple Machine Learning Research
Help Net Security
Help Net Security
Attack and Defense Labs
Attack and Defense Labs
N
Netflix TechBlog - Medium
Spread Privacy
Spread Privacy
F
Full Disclosure
Recorded Future
Recorded Future
AWS News Blog
AWS News Blog
博客园 - 【当耐特】
The Cloudflare Blog
T
Threatpost
T
Tor Project blog
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recent Announcements
Recent Announcements
M
MIT News - Artificial intelligence
A
Arctic Wolf
C
Check Point Blog
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
小众软件
小众软件
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Proofpoint News Feed
Security Latest
Security Latest
The Last Watchdog
The Last Watchdog

Dizzy zone

Pangolin Private Resources With Domain Https About n8n and large files Malicious Node install script on Google search Wrapping Go errors with caller info BLAKE2b performance on Apple Silicon State of my Homelab 2025 My homelabs power consumption On Umami ML for related posts on Hugo Probabilistic Early Expiration in Go SQLC & dynamic queries Enums in Go Streaming Netdata metrics from TrueNAS SCALE SQL string constant gotcha Moving from Jenkins to Drone My new server: MSI Cubi 3 Silent My thoughts on Ansible® Profiling gin with pprof How I host this blog, CI and tooling Refactoring Go switch statements OAuth with Gin and Goth I made my own commenting server. Here's why. Why I hate OpenApi(swagger) IDE for GO Jenkins on raspberry pi 3 How I started my professional career Kestrel vs Gin vs Iris vs Express vs Fasthttp on EC2 nano Go's defer statement Self-hosted disqus alternative for 5$ a month Why I like go Speeding hexo (or any page) for PageSpeed insights Starting a blog with hexo and AWS S3
Redis is fast - I'll cache in Postgres
Vik · 2025-09-24 · via Dizzy zone

There are books & many articles online, like this one arguing for using Postgres for everything. I thought I’d take a look at one use case - using Postgres instead of Redis for caching. I work with APIs quite a bit, so I’d build a super simple HTTP server that responds with data from that cache. I’d start from Redis as this is something I frequently encounter at work, switch it out to Postgres using unlogged tables and see if there’s a difference.

The setup

I’ll run the experiment on my homelab’s k8s cluster. The idea is to run Postgres or Redis on one node, limiting it to 2CPUs via k8s limits, as well as 8GiB of memory. On another node, I’ll run the web server itself and then spin a pod for the benchmark executed via k6 on the third.

Both postgres and redis are used with the out of the box settings for the following images:

  • Postgres - postgres:17.6
  • Redis - redis:8.2

I wrote a simple webserver, with 2 endpoints, a cache and a “Session” struct which we’ll store in the cache:

var ErrCacheMiss = errors.New("cache miss")

type Cache interface {
	Get(ctx context.Context, key string) (string, error)
	Set(ctx context.Context, key string, value string) error
}

type Session struct {
	ID string
}


func serveHTTP(c Cache) {
	http.HandleFunc("/get", getHandler(c))
	http.HandleFunc("/set", setHandler(c))

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	fmt.Println("Server starting on http://0.0.0.0:" + port)

	server := &http.Server{Addr: "0.0.0.0:" + port}

	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Println("Error starting server:", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit

	fmt.Println("Shutting down server...")

	if err := server.Close(); err != nil {
		fmt.Println("Error shutting down server:", err)
	}
}

For redis, I’ve implemented the cache using github.com/redis/go-redis/v9 as follows:

type RedisCache struct {
	client *redis.Client
}

func NewRedisCache() *RedisCache {
	redisURL := os.Getenv("REDIS_URL")
	if redisURL == "" {
		redisURL = "localhost:6379"
	}

	fmt.Println("Connecting to Redis at", redisURL)

	client := redis.NewClient(&redis.Options{
		Addr:     redisURL,
		Password: "",
		DB:       0,
	})

	return &RedisCache{
		client: client,
	}
}

func (r *RedisCache) Get(ctx context.Context, key string) (string, error) {
	val, err := r.client.Get(ctx, key).Result()
	if err == redis.Nil {
		return "", ErrCacheMiss
	}
	if err != nil {
		return "", err
	}
	return val, nil
}

func (r *RedisCache) Set(ctx context.Context, key string, value string) error {
	return r.client.Set(ctx, key, value, 0).Err()
}

The postgres cache is implemented using the github.com/jackc/pgx/v5 library:

type PostgresCache struct {
	db *pgxpool.Pool
}

func NewPostgresCache() (*PostgresCache, error) {
	pgDSN := os.Getenv("POSTGRES_DSN")
	if pgDSN == "" {
		pgDSN = "postgres://user:password@localhost:5432/mydb"
	}

	cfg, err := pgxpool.ParseConfig(pgDSN)
	if err != nil {
		return nil, err
	}

	cfg.MaxConns = 50
	cfg.MinConns = 10

	pool, err := pgxpool.NewWithConfig(context.Background(), cfg)
	if err != nil {
		return nil, err
	}

	_, err = pool.Exec(context.Background(), `
		CREATE UNLOGGED TABLE IF NOT EXISTS cache (
			key VARCHAR(255) PRIMARY KEY,
			value TEXT
		);
	`)
	if err != nil {
		return nil, err
	}

	return &PostgresCache{
		db: pool,
	}, nil
}

func (p *PostgresCache) Get(ctx context.Context, key string) (string, error) {
	var content string
	err := p.db.QueryRow(ctx, `SELECT value FROM cache WHERE key = $1`, key).Scan(&content)
	if err == pgx.ErrNoRows {
		return "", ErrCacheMiss
	}
	if err != nil {
		return "", err
	}
	return content, nil
}

func (p *PostgresCache) Set(ctx context.Context, key string, value string) error {
	_, err := p.db.Exec(ctx, `INSERT INTO cache (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2`, key, value)
	return err
}

I’ll seed the redis and postgres with 30 million entries each, keeping record of the inserted uuids. From there, I’ll generate a subset of existing uuids to use while benchmarking. This allows for simulating both hits and misses.

I’ll do a few runs of benchmarks for gets first, then sets and then a mixed run. Each run will execute for 2 minutes. I’ll look at the number of operations per second, latencies as well as memory and CPU usage during those times.

To simulate a somewhat real scenario where only a subset of keys exist in the cache the set benchmark will have a 10% chance to update an existing key, whereas the get will have an 80% chance of picking an existing key. The mixed workload will have a 20% chance to execute a set scenario and 80% for the get scenario.

The results

Getting values from cache

Requests per second – higher is better

Redis performed better than Postgres, which did not surprise me at all. The bottleneck was actually the HTTP server. The machine running the http server maxed out on CPU, with redis running comfortably with ~1280mCPU - short of the 2000mCPU limit imposed. Redis used ~3800MiB of RAM, which stayed flat across the runs.

For postgres, the bottleneck was the CPU on postgres side. It consistently maxed out the 2 cores dedicated to it, while also using ~5000MiB of RAM.

Redis also did better when it comes to latencies of the HTTP responses:

Latency, milliseconds – lower is better

Setting values in cache

Requests per second – higher is better

Once again Redis performed better. The CPU usage stayed roughly the same as in the case of the GET experiment, with the RAM usage growing to ~4300MiB due to the new keys being inserted. The bottleneck stayed on the HTTP server side, with Redis using ~1280mCPU once again.

Postgres once again was bottlenecked by the CPU, constantly using 100% of the 2 cores it was limited to. During the course of the run, the memory usage grew to ~5500MiB.

During the test, the endpoints with the Redis cache implementation also had better latencies:

Latency, milliseconds – lower is better

Read/write performance

Requests per second – higher is better

The mixed benchmark also returned the predictable result of Redis reigning superior. As has been the story so far, the CPU stayed put at ~1280mCPU, RAM usage grew a bit due to the new keys being inserted.

Postgres maxed out the two cores and reached around 6GiB of memory used.

Latencies once again were better when using redis:

Latency, milliseconds – lower is better

Unlogged tables

In the benchmark, I’ve used an unlogged table for postgres but this has not seemed to help, or has it? If I rerun the same benchmark with a normal(logged) table we can look at the numbers.

Requests per second – higher is better

The unlogged table makes a huge difference for the write benchmark and a somewhat smaller but still significant one for the mixed workload. This is because the unlogged tables skip the write ahead log making them a lot faster for writes. There’s very little difference for the read performance though and I expect more runs would show the two test cases converging.

Conclusion

Redis is faster than postgres when it comes to caching, there’s no doubt about it. It conveniently comes with a bunch of other useful functionality that one would expect from a cache, such as TTLs. It was also bottlenecked by the hardware, my service or a combination of both and could definitely show better numbers. Surely, we should all use Redis for our caching needs then, right? Well, I think I’ll still use postgres. Almost always, my projects need a database. Not having to add another dependency comes with its own benefits. If I need my keys to expire, I’ll add a column for it, and a cron job to remove those keys from the table. As far as speed goes - 7425 requests per second is still a lot. That’s more than half a billion requests per day. All on hardware that’s 10 years old and using laptop CPUs. Not many projects will reach this scale and if they do I can just upgrade the postgres instance or if need be spin up a redis then. Having an interface for your cache so you can easily switch out the underlying store is definitely something I’ll keep doing exactly for this purpose.

Thanks for reading!