Skip to content

perf(backend): clustering + pool-config + auth-cache voor ~100 gelijktijdige gebruikers#350

Open
robbertbos wants to merge 2 commits into
experimenteel/samenwerkenfrom
perf/backend-concurrency-scaling
Open

perf(backend): clustering + pool-config + auth-cache voor ~100 gelijktijdige gebruikers#350
robbertbos wants to merge 2 commits into
experimenteel/samenwerkenfrom
perf/backend-concurrency-scaling

Conversation

@robbertbos

@robbertbos robbertbos commented Jun 8, 2026

Copy link
Copy Markdown
Member

Doel

Een collega meldde dat de applicatie >5s traag reageert. Naast de diagnose is de eis: ~100 gelijktijdige gebruikers aankunnen én de DB-laag goed afgestemd hebben. Dit is PR1 ("kern eerst"); PR2 (#351) is gestapeld hierop.

Wijzigingen

  • Expliciete DB-pool (DB_POOL_MAX, DB_CONNECT_TIMEOUT, DB_IDLE_TIMEOUT) met input-clamping. Default DB_POOL_MAX=15, ceiling 20 — afgestemd op de RIG-Postgres (zie onder).
  • Auth-cache (oidcSub → intern id) zodat pollende clients niet elke request een users-lookup doen. Cachet alléén het id (geen PII), TTL ≤ token-exp, begrensd + evictie, fail-safe.
  • Clustering (node:cluster, WEB_CONCURRENCY) — standaard 1 worker (opt-in). De app is I/O-bound (lage CPU-load), dus één proces met een gezonde pool is de beste balans; clustering aanzetten kan via WEB_CONCURRENCY > 1 mét een lagere pool. Rate-limit-max wordt over de workers verdeeld (per-worker in-memory store).

DB-connectielimiet (RIG-Postgres) — belangrijk

De gedeelde rig-db staat op max_connections: 250 / reserved_connections: 10, en elke project-DB-user is gecapt op 20 connecties totaal (CONNECTION LIMIT 20). Het budget over álle pods/replica's/workers samen is dus 20. Rekensom (zie README):

replicas × WEB_CONCURRENCY × DB_POOL_MAX  ≤  ~18

De tweede commit corrigeert de defaults hierop (de oorspronkelijke 1 worker/core × pool 10 zou de cap overschrijden). Voor echte schaal onder deze cap is PgBouncer de route (al voorzien als rig-cluster-future).

Security/standaarden-audit

Vooraf getoetst tegen BIO/NeRDS/AVG/OIDC + NL GOV ADR/internet.nl/Logboek (multi-agent). Mitigaties ingebouwd: cache alléén id, TTL ≤ exp, begrensd, fail-safe; tokenvalidatie + autorisatie blijven per request (intrekken werkt direct); env-clamping; rate-limit per worker verdeeld.

Verificatie

  • 383 tests, 100% coverage; tsc --noEmit schoon; geen nieuwe dependencies.
  • Clustering runtime-geverifieerd (2 workers, gedeelde poort, round-robin, health 200, auth 401).

Follow-ups

PR2 (#351): JWKS warm-up, compressie, save-transactie, sync Promise.all. Pre-existing hardening-backlog: azp vs aud + jti-replaycheck, Logboek Dataverwerkingen, ADR /openapi.json-pad, CSP unsafe-inline, pino redact; PgBouncer.

…tijdige gebruikers

Schaalt de Fastify-backend naar ~100 gelijktijdige gebruikers en benut de
beschikbare CPU, met de mitigaties uit de security/standaarden-audit ingebouwd.

- Clustering (node:cluster, WEB_CONCURRENCY, default 1 worker/core; 1 = uit).
  WEB_CONCURRENCY geclampt op [1,64] tegen een fork-bom; crashende workers
  herstarten. De rate-limit-max wordt over de workers gedeeld (per-worker
  in-memory store), zodat de cluster-brede limiet bij benadering gelijk blijft.
- Expliciete postgres-pool (DB_POOL_MAX/connect/idle-timeout) uit env, met
  clamping. README documenteert de workers x DB_POOL_MAX <= max_connections rekensom.
- Auth-cache (oidcSub -> intern id) zodat pollende clients niet elke request een
  users-lookup triggeren. Cachet ALLEEN het id (geen persoonsgegevens), TTL <=
  token-exp, begrensd + evictie. Tokenvalidatie (sig/issuer/azp/exp) en
  autorisatie blijven per request: een cache-hit kan dus geen toegang lekken.

383 tests, 100% coverage. Clustering runtime-geverifieerd (2 workers, round-robin).
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Your changes have been deployed to a preview environment:

api: https://pr-350-asses-k2n.rig.prd1.gn2.quattro.rijksapps.nl

frontend: https://pr-350-asses-k2n.rig.prd1.gn2.quattro.rijksapps.nl

This deployment will be automatically cleaned up when the PR is closed.

robbertbos added a commit that referenced this pull request Jun 8, 2026
Vervolg-quick-wins op PR1 (#350), drie onafhankelijke verbeteringen:

- JWKS: expliciete timeoutDuration 2500ms (i.p.v. de jose-default 5000ms) plus
  een best-effort warmUpJwks() bij worker-start, zodat de eerste geauthenticeerde
  request niet de koude Keycloak-fetch betaalt. Tokenvalidatie ongewijzigd.
- @fastify/compress (global) voor grote JSON-responses.
- sync-poll: de twee onafhankelijke queries parallel via Promise.all.

De atomische save (db.transaction) is hier komen te vervallen: die wordt gedekt
door de save-race-fix #353 (transactie + conditional UPDATE), inmiddels gemerged.

100% coverage.
robbertbos added a commit that referenced this pull request Jun 8, 2026
Vervolg-quick-wins op PR1 (#350), drie onafhankelijke verbeteringen:

- JWKS: expliciete timeoutDuration 2500ms (i.p.v. de jose-default 5000ms) plus
  een best-effort warmUpJwks() bij worker-start, zodat de eerste geauthenticeerde
  request niet de koude Keycloak-fetch betaalt. Tokenvalidatie ongewijzigd.
- @fastify/compress (global) voor grote JSON-responses.
- sync-poll: de twee onafhankelijke queries parallel via Promise.all.

De atomische save (db.transaction) is hier komen te vervallen: die wordt gedekt
door de save-race-fix #353 (transactie + conditional UPDATE), inmiddels gemerged.

100% coverage.
robbertbos added a commit that referenced this pull request Jun 8, 2026
Vervolg-quick-wins op PR1 (#350), drie onafhankelijke verbeteringen:

- JWKS: expliciete timeoutDuration 2500ms (i.p.v. de jose-default 5000ms) plus
  een best-effort warmUpJwks() bij worker-start, zodat de eerste geauthenticeerde
  request niet de koude Keycloak-fetch betaalt. Tokenvalidatie ongewijzigd.
- @fastify/compress (global) voor grote JSON-responses.
- sync-poll: de twee onafhankelijke queries parallel via Promise.all.

De atomische save (db.transaction) is hier komen te vervallen: die wordt gedekt
door de save-race-fix #353 (transactie + conditional UPDATE), inmiddels gemerged.

100% coverage.
…-cap per user

De gedeelde RIG-Postgres (rig-db) capt elke project-DB-user op 20 connecties
totaal (CONNECTION LIMIT 20; max_connections 250, reserved 10). De vorige
defaults (1 worker per core × pool 10) zouden dat budget onder clustering ruim
overschrijden en connecties laten weigeren.

- WEB_CONCURRENCY default 1 (clustering opt-in i.p.v. één worker per core). De
  app is I/O-bound, dus één proces met een gezonde pool is de beste balans.
- DB_POOL_MAX default 9, ceiling 20. Een rolling deploy draait kort 2 pods, dus
  2 × 1 worker × 9 = 18 — een krappe marge onder de cap van 20.
- README: budget pods × WEB_CONCURRENCY × DB_POOL_MAX ≤ 20, plus PgBouncer als
  route voor echte schaal.
@robbertbos robbertbos force-pushed the perf/backend-concurrency-scaling branch from 37c7b54 to 88ba803 Compare June 8, 2026 21:23
robbertbos added a commit that referenced this pull request Jun 8, 2026
Vervolg-quick-wins op PR1 (#350), drie onafhankelijke verbeteringen:

- JWKS: expliciete timeoutDuration 2500ms (i.p.v. de jose-default 5000ms) plus
  een best-effort warmUpJwks() bij worker-start, zodat de eerste geauthenticeerde
  request niet de koude Keycloak-fetch betaalt. Tokenvalidatie ongewijzigd.
- @fastify/compress (global) voor grote JSON-responses.
- sync-poll: de twee onafhankelijke queries parallel via Promise.all.

De atomische save (db.transaction) is hier komen te vervallen: die wordt gedekt
door de save-race-fix #353 (transactie + conditional UPDATE), inmiddels gemerged.

100% coverage.
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.

1 participant