perf: cut Postiz memory footprint for single-tenant use (v0.3.0) - #3
Merged
Conversation
Bump to v0.3.0. The container was pinned against its 2g cgroup limit with 1GB in swap, having hit memory.max 133,238 times and been OOM-killed once. Restarting it reclaimed nothing — it returned to 2015M with 841M swapped inside four minutes, which rules out a leak: this is a container sized for a workload it does not have. Actual scale is 1 user, 6 integrations, 461 lifetime posts. Three changes, in order of impact: 1. POSTIZ_ACTIVE_PROVIDERS (see fatherlinux/postiz-app@crunchtools-patches) trims Temporal workers from 31 to 7. This is the ~700MB of native memory that the existing --max-old-space-size=384 could never reach, since it lives in the SDK's Rust core rather than the V8 heap. 2. ecosystem.config.js runs node directly instead of pnpm -> dotenv -> node, dropping six wrapper processes (~195MB). The dotenv layer reads /app/.env, which this image never creates, so it was loading nothing. 3. Per-app heap caps via PM2 interpreter_args, and Temporal/postgres connection pools sized for one tenant. A CLI flag overrides an inherited NODE_OPTIONS (verified: 128 yields a 176MB ceiling vs 432MB from the env), so per-app limits win over the global. postiz-pg-init.sh only runs on first init, so its new values do not affect the existing volume; that postgresql.conf is edited separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dGq9ivDC6sqeJJfkra6C2
Testing the staged config against a disposable container caught this before
it shipped. Lowering max_connections to 30 leaves no headroom:
- Prisma defaults to num_cpus*2+1 per client and DATABASE_URL sets no
connection_limit, so on 6 vCPUs that is 13 each for the backend and the
orchestrator = 26.
- Temporal opens a pool PER SERVICE (frontend/history/matching/worker),
not one global pool, so maxConns in config.yaml is not the ceiling.
Measured 13 at idle in production, 12 in the test container.
- Background workers = 4.
Worst case ~43 against an idle baseline of 22. At 30 this would have failed
with "sorry, too many clients already" under any real load.
Fresh installs now get 60 with the arithmetic documented inline. The
shared_buffers reduction to 128MB is unaffected and stays — it has no bearing
on connection limits.
The Temporal pool reduction (10/5 -> 6/2, 5/3 -> 4/1) is kept: the test
container ran clean on it with zero pool-exhaustion errors. Worth noting its
measured benefit is roughly one connection, so it is close to neutral. The
memory win in this release is entirely the worker-count and wrapper-process
changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dGq9ivDC6sqeJJfkra6C2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The container was pinned against its 2g cgroup limit with 1GB in swap — it had hit
memory.max133,238 times and been OOM-killed once. A restart reclaimed nothing: it returned to 2015M with 841M swapped within four minutes, which rules out a leak. This is a container sized for a workload it does not have.Actual scale: 1 user, 6 integrations, 461 lifetime posts.
Root cause
temporal.module.tsregisters one Temporal worker per supported provider rather than per connected one — 31 workers. Each carries its own@temporalio/core-bridgeRust core and thread pair, allocated outside the V8 heap where--max-old-space-sizecannot bound it.Confirmed by thread accounting in the running orchestrator:
workflow-procestemporal-real-stokio-runtime-wnodelibuv-worker117 total — exactly one
workflow-proces/temporal-real-spair per worker, against 1,088,416 kB of private dirty anonymous memory with the heap capped at 432MB.Changes
POSTIZ_ACTIVE_PROVIDERS(infatherlinux/postiz-app@crunchtools-patches) trims workers 31 → 7. Unset preserves upstream behaviour exactly. Also pinsmaxCachedWorkflowsinstead of inheriting the SDK default, which derives fromheap_size_limitand lands near 135 per worker.ecosystem.config.jsrunsnodedirectly instead ofpnpm→dotenv→node, dropping six wrapper processes (~195MB). The dotenv layer reads/app/.env, which this image never creates.interpreter_args, plus Temporal/postgres pools sized for one tenant.Test plan
ecosystem.config.jsparses; all three apps resolve to directnodeinvocations with distinct heap flagsmainalways present, typos and stray whitespace handlednestjs-temporal-coreforwardsworkerOptionsintoWorker.createviaObject.assign(temporal-worker.service.js:182-186)NODE_OPTIONS(128 → 176MB ceiling vs 432MB from env)Caveat
A provider absent from
POSTIZ_ACTIVE_PROVIDERShas nobody polling its task queue, so its posts enqueue and never execute. The boot log prints the active set and warns on unknown identifiers. Add the identifier and restart when connecting a new provider.Note:
postiz-pg-init.shonly runs on first init, so its new values do not affect the existing volume — thatpostgresql.confis edited separately.