Skip to content

PgBouncer connection pooling and horizontal scaling - #10

Merged
BackStacked merged 4 commits into
mainfrom
claude/project-thread-eltnqk
Oct 16, 2026
Merged

BackStacked merged 4 commits into
mainfrom
claude/project-thread-eltnqk

Conversation

@BackStacked

Copy link
Copy Markdown
Contributor

Requested by Himanshu · project thread

Before: the api connected straight to PostgreSQL with Sequelize's default pool, every process created tables on start, GET /health only checked the database, and SIGTERM closed the server at once. Running several replicas was described but not tested or packaged.

After: the api runs as N replicas behind a proxy, connecting through PgBouncer in transaction mode. docker-compose.yml brings up PostgreSQL, PgBouncer, Redis, a one-shot table sync, API_REPLICAS api containers, the worker and nginx. Replicas drain cleanly on SIGTERM, and /health tells the load balancer when a replica can't serve.

How:

  • PgBouncer. DATABASE_URL points at PgBouncer; no code change was needed in the db layer. db-kit uses unnamed statements only, FOR UPDATE and isolation inside one transaction, one-statement counters, and no advisory locks, LISTEN, or temp tables. Sequelize's connect-time SET TIME ZONE is a parameter PgBouncer tracks per client. npm test now runs the whole PostgreSQL suite a second time through PgBouncer (transaction mode, 3 server connections).
  • Pools. DB_POOL_MAX / DB_POOL_MIN / DB_POOL_IDLE_MS / DB_POOL_ACQUIRE_MS size the Sequelize pool (pg) or the MongoDB driver pool. DB_SYNC=false plus node dist/sync.js (npm run db:sync) stops replicas racing to create tables.
  • Replicas. A new [replicas] test runs two servers on one PgBouncer and one Redis. A session, a refresh and a logout each cross replicas, and login limits are shared (limited at attempt 11, not 21). 16 concurrent invoices with payments from both replicas get unique numbers and the books balance. PostgreSQL saw at most 3 connections.
  • Readiness and shutdown (src/lifecycle.ts). GET /health returns 503 when the database or Redis fails (2 s timeout each) or the replica is draining. GET /health?live=1 is liveness. This stays within the one-route rule. On SIGTERM the replica drains for SHUTDOWN_DRAIN_MS, then app.close() finishes in-flight requests, and it exits anyway after SHUTDOWN_TIMEOUT_MS. The worker gets the same timeout.
  • Packaging. Adds a Dockerfile (kits token as a build secret), docker-compose.yml and deploy/nginx.conf, which re-resolves api so --scale needs no restart and retries a request on another replica after a 503. CI installs pgbouncer.
  • Docs. New docs/17-scaling.md covers what each replica needs, where state lives, PgBouncer compatibility, pool sizing, health, shutdown, compose and Kubernetes. Also updates .env.example, docs 09/11/12 and AGENTS.md (rule 14).

Verified locally: typecheck; TEST_BACKENDS=pg npm test 193/193 (MongoDB binaries are blocked in this sandbox, so CI runs mongo). The compose stack came up with 3 healthy replicas through nginx. Stopping one replica under constant traffic answered 40 of 40 requests with 200.

Overlap with #9 (TLS fingerprint): both PRs touch .env.example, src/config/env.ts, src/server.ts, test/run.ts, docs 09/12 and llms-full.txt, in different hunks. Whichever merges second needs a rebase and npm run llms.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dcqyj37vsGrmgdnp7DLbkn


Generated by Claude Code

- DATABASE_URL may point at PgBouncer; transaction pooling verified: npm test runs the
  whole PostgreSQL suite again through PgBouncer, then two replicas sharing PgBouncer
  and Redis (sessions, refresh, logout, login limits, concurrent invoices, shutdown)
- DB_POOL_MAX/MIN/IDLE_MS/ACQUIRE_MS size the Sequelize / MongoDB pools; DB_SYNC and
  node dist/sync.js (npm run db:sync) so replicas don't race to create tables
- GET /health is readiness (database, Redis, draining → 503); /health?live=1 is liveness
- graceful shutdown on SIGTERM: drain (SHUTDOWN_DRAIN_MS), finish requests, close,
  hard exit after SHUTDOWN_TIMEOUT_MS; the worker gets the same timeout
- Dockerfile, docker-compose.yml (PostgreSQL, PgBouncer, Redis, db-sync, N api replicas,
  worker, nginx), deploy/nginx.conf; CI installs pgbouncer
- docs/17-scaling.md, .env.example, docs 09/11/12, AGENTS.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dcqyj37vsGrmgdnp7DLbkn
@BackStacked BackStacked self-assigned this Oct 15, 2026
@BackStacked
BackStacked marked this pull request as ready for review October 15, 2026 21:15
… the repo table

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dcqyj37vsGrmgdnp7DLbkn
@BackStacked
BackStacked merged commit caadedf into main Oct 16, 2026
1 check passed
@BackStacked
BackStacked deleted the claude/project-thread-eltnqk branch October 16, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants