SZTech Notes

Personal notes on backend engineering & infrastructure

March 12, 2026

Connection Pooling in PostgreSQL: PgBouncer vs Pgpool-II in 2026

After migrating three production services to PostgreSQL 17, I finally had time to benchmark our connection pooling setup properly. The short version: PgBouncer in transaction mode is still the right choice for most workloads, but there are edge cases where Pgpool-II's query caching gives you meaningful gains.

The test setup was straightforward — a 4-core VM with 8GB RAM running PG17 on Debian 12, a separate benchmarking host running pgbench with 200 concurrent connections, and each pooler configured with a pool size of 25. Nothing exotic.

What surprised me was the latency distribution under sustained load. PgBouncer showed p99 latencies of ~12ms for simple SELECT queries, while Pgpool-II hovered around 18ms for the same workload. But once we added a mix of complex JOINs and the query cache warmed up, Pgpool-II's p50 dropped to 4ms compared to PgBouncer's consistent 8ms.

# PgBouncer config that worked best for us
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 400
default_pool_size = 25
reserve_pool_size = 5
server_idle_timeout = 300

The real takeaway: if your application does mostly unique queries with variable parameters, stick with PgBouncer. If you have a read-heavy workload with repeated query patterns, Pgpool-II deserves another look. We ended up keeping PgBouncer for our write-heavy services and testing Pgpool-II for the reporting backend.

postgresql infrastructure benchmarks

February 28, 2026

Replacing Cron with systemd Timers: A Practical Migration

I have been putting this off for years but finally migrated all our cron jobs to systemd timers last month. The trigger was a silent cron failure that went unnoticed for 11 days — a log rotation script that stopped running because of a PATH issue after a system update. With systemd timers, that failure would have been immediately visible in systemctl --failed.

The migration itself was mechanical. Each cron entry becomes two files: a .service unit describing what to run, and a .timer unit describing when to run it. The verbosity is annoying at first, but the built-in logging, dependency management, and resource controls make it worth it.

# /etc/systemd/system/backup-db.service
[Unit]
Description=Daily database backup
After=postgresql.service

[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-postgres.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/backup-db.timer
[Unit]
Description=Run DB backup daily at 3:00 UTC

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

The Persistent=true flag is the key feature that cron lacks — if the server was down when the timer should have fired, systemd will run it at the next boot. Combined with RandomizedDelaySec to avoid thundering herd on multi-server setups, this is a strictly better solution for scheduled tasks.

Total migration time for 14 cron jobs across 6 servers: about 4 hours. Should have done it years ago.

systemd linux devops

February 14, 2026

Notes on Switching from Nginx to Caddy for Reverse Proxy

This one started as a quick experiment and ended up becoming permanent. We had a staging environment running Nginx as a reverse proxy for 5 backend services, and the certificate renewal setup (certbot + cron + reload hooks) broke after an OS upgrade. Rather than debug it again, I decided to test Caddy as a drop-in replacement.

The automatic HTTPS alone justified the switch. No certbot, no renewal cron, no reload scripts — Caddy handles it all internally via ACME. The Caddyfile syntax is refreshingly simple compared to nginx.conf, though the JSON config format gives you more control for complex setups.

Performance was comparable. Under our typical load (~2000 req/s at the proxy layer), both Caddy and Nginx showed nearly identical latency distributions. Caddy used slightly more memory (~40MB vs ~25MB for Nginx), which is irrelevant for a proxy but worth noting.

The main downside: fewer StackOverflow answers when something goes wrong. The Caddy community forums are helpful, but you'll occasionally run into configurations where the Nginx solution is a one-liner and the Caddy equivalent requires creative thinking.

caddy nginx reverse-proxy

January 30, 2026

Docker Compose V2: Three Gotchas That Wasted My Afternoon

We finally removed the last docker-compose (v1) invocation from our CI pipeline last week. The migration to docker compose (v2, integrated as a Docker CLI plugin) was mostly seamless, but three issues cost me a combined ~4 hours of debugging.

First, container naming. V1 used underscores (project_service_1), V2 uses hyphens (project-service-1). Any script that referenced containers by name broke silently. Second, the --compatibility flag in V2 doesn't behave identically to V1's deploy section handling — we had resource limits that were silently ignored. Third, depends_on with health checks now requires explicit condition: service_healthy, which was the default behavior in V1 with a healthcheck defined.

None of these are bugs — they're documented breaking changes. But combined, they made what should have been a 20-minute sed replacement into an afternoon of testing.

docker containers devops