Two reference variants
We ship two sets of resource defaults. Treat them as the recommended lower and upper bounds for production; your environment should land somewhere between the two, tuned to your actual load and use case.Start at Balanced.
Balanced is now the chart default and the right starting point for a fresh install: high enough to run a real workload comfortably, low enough to avoid over-paying before you know your traffic shape. Move toward Performance (or somewhere in between) once you’ve observed your actual usage.Trade-off in plain English
- High requests → fewer pods per node → idle CPU/RAM is wasted when pods aren’t busy.
- Low requests → more pods per node → risk of a node being unable to provide CPU/RAM under load spikes.
Right-sizing from observed usage
Once the platform is live, monitor per-pod CPU and memory and resize from data, not from these tables:- CPU request → set to the P95 of the pod’s normal usage. CPU is compressible: the occasional P99 spike is absorbed by the node without breaking the workload, so paying for it as a permanent request wastes capacity.
- Memory request → set to the P99 of the pod’s normal usage. Memory is not compressible: exceeding the request risks eviction or OOMKill, so the request must comfortably cover the worst legitimate spike.
- Memory limit → keep close to the request (the chart defaults already do this) so a runaway pod is capped before it OOMs the node.
- CPU limit → don’t set one (see Why no CPU limit? below).
Cluster size baseline
Our reference node is 4 vCPU / 16 GB RAM (a common cloud baseline like AWSm6i.xlarge, GCP n2-standard-4, Azure D4s_v5). Once the cloud provider’s reservations and the usual DaemonSets are subtracted, such a node leaves roughly 3.6–3.8 vCPU and ~14 GB for Prisme.ai pods (see Fitting the runtime on a node).
Scheduling the requests / limits below on these nodes gives:
How to read this table:
- CPU is counted on requests, memory on limits. CPU is compressible: the scheduler places pods on their requests and a busy pod is only slowed down. Memory is not: a pod exceeding its limit is killed, and a node whose pods together exceed its RAM evicts them. Plan node memory on the sum of the limits so you never overcommit it.
- CPU is the constraint in both variants; memory never is on 16 GB nodes.
- Performance needs 12 nodes, not 10. A Performance
runtimepod takes almost a whole 4 vCPU node. 10 nodes only host the HPA minimums if every pod is packed perfectly, and leave no room for a rolling update or a scale-out. In practice the Kubernetes scheduler spreads pods across the least loaded nodes, which scatters small pods over empty nodes and leaves fewer whole nodes free forruntimeandcrawler. Start with 12 nodes and keep the cluster autoscaler enabled: it adds a node whenever a large pod finds no room. - The starting pool is deliberately smaller than what HPA max needs. Provisioning every node HPA could ever ask for would waste capacity most of the time; the cluster autoscaler adds nodes as HPA scales out and removes them afterwards. It is required in both variants. The
HPA maxvalues are themselves yours to raise (see HPA), so no fixed node count can cover them.
Alternative: 8 vCPU nodes for Performance
On 8 vCPU / 32 GB nodes (AWSm6i.2xlarge, GCP n2-standard-8, Azure D8s_v5), or compute-optimized 8 vCPU / 16 GB nodes (Performance needs roughly 1 GB of memory limit per vCPU of request), the Performance variant fits in 5 nodes at HPA min and ~11 at HPA max. A runtime pod then shares its node with other pods, so you can raise its CPU request to 4–4.5 vCPU (the technical ceiling of 4 workers, see Scaling prismeai-runtime workers).
Both are supported. 4 vCPU nodes remain the reference, and Performance runs fine on them; 8 vCPU nodes are a comfort option for large, bursty loads, not a prerequisite.
Core namespace
Variant A: Balanced
Variant B: Performance
The Performance
runtime request (3500m) is the largest value that still fits a 4 vCPU node on the major clouds once their reservations and DaemonSets are subtracted. Check it against your own nodes before applying it: see Fitting the runtime on a node.Apps namespace
Variant A: Balanced
Variant B: Performance
Why no CPU limit?
You’ll notice the tables above have no CPU limit. Don’t set one on Prisme.ai services.- The Linux CFS scheduler already throttles pods that exceed their request when another pod on the same node needs that CPU.
- A CPU limit on top of that wastes free CPU: when the node has spare cycles and a pod could use them, the limit blocks it for no reason.
- In practice, CPU limits on Node.js workloads cause unnecessary tail-latency spikes and add zero protection.
Horizontal Pod Autoscaling (HPA)
Golden rule for production: at least 2 replicas of every core service, and offunctions and crawler in apps. Single-replica means a single bad pod, restart or node drain takes the service down.
The
HPA max values in the variant tables above are starting points, not ceilings. They were chosen to cover the typical load profile of each variant; review them against your own observed traffic and raise them when you see HPA sitting at max replicas during peaks or latency degrading. The right max replicas count is the one that comfortably absorbs your highest expected spike with headroom; treat the defaults as a starting baseline to tune.Example HPA config
Recommended targets
Scaling prismeai-runtime workers
prismeai-runtime is the only platform service that runs multiple workers inside the same pod. The pod runs a main thread that receives HTTP calls from api-gateway and events from the broker and dispatches them to N worker threads which actually execute the automations. Adding workers raises the per-pod parallelism without spinning up new pods.
Configured via the Helm value prismeai-runtime.maxWorkers (which populates the RUNNER_MAX_THREADS env var inside the container). Default: 1.
The
runtime resource numbers in the variant tables above are sized for a specific worker count: 3 workers for Balanced, 4 workers for Performance. If you change maxWorkers, re-derive the pod resources with the math below; keeping the defaults while bumping workers will starve each one.Golden rule: scale workers and resources together
A worker is a Node.js thread; it competes for the pod’s CPU and memory budget alongside the main thread. DoublingmaxWorkers without raising resources.requests / resources.limits halves what each worker gets and you’ll see latency degrade or workers OOMKilled.
The chart slices 75% of resources.limits.memory across the workers (the other 25% goes to V8 metadata, native modules and the main thread). So sizing the pod is a two-step decision:
- Pick how much heap each worker needs based on the workload it will execute. For production workloads, plan 500 MiB to 1 GiB per worker depending on the automations they run (heavier templating, large payloads or heavy in-flight contexts push it toward 1 GiB).
- Set
limits.memoryandmaxWorkerstogether so the chart’s auto-slice gives each worker that heap.
- Top-down: keep
limits.memoryfixed and lowermaxWorkers; each worker’s heap rises proportionally. - Bottom-up: target a per-worker heap and raise
limits.memoryto fitmaxWorkersof them, plus the 25% main-thread overhead.
limits.memory ≈ 4 GiB, maxWorkers: 4 (this is the Performance default).
CPU per worker
A worker (i.e. a Node.js thread) can use at most one core, and the main thread a small share on top. So one core per worker plus ~0.5 for the main thread is the ceiling a runtime pod can consume: about4.5 vCPU for 4 workers. A request above that is wasted.
It is a ceiling, not a minimum. Automations spend much of their time waiting on I/O (LLM calls, HTTP fetches, databases), so workers rarely all burn a full core at once, and a request below one core per worker is normal: Balanced runs 3 workers on 2 vCPU. Size the request from the observed P95 usage, like any other service.
On a 4 vCPU node, the 4.5 vCPU ceiling can’t be requested: keep maxWorkers: 4 and request what the node can actually give (3500m in the Performance table). Because Prisme.ai sets no CPU limit and a Performance runtime pod is alone on its node, a lower request doesn’t take CPU away from the workers: they still use all the CPU the node has free. Dropping to 3 workers caps automation execution at 3 cores, whatever the node has free. With 4 workers, the pod also uses the remaining 0.5–0.7 vCPU of requestable CPU, and usually more: DaemonSets and system pods rarely consume their whole request, and that idle CPU goes to whoever needs it. A 4th worker also limits how much work waits behind a CPU-heavy automation: tasks are dispatched round-robin and each worker runs its automations concurrently on one event loop, so a worker busy with a heavy automation delays every task assigned to it. In return, each worker gets less heap and the pod opens more database connection pools. The 3-worker option pays off when you also lower the request (about 3 vCPU) so that a small pod can share the node.
Fitting the runtime on a node
A pod is only scheduled if its request fits in what the node has left. On a 4 vCPU node, that is less than 4 vCPU:- Allocatable CPU is the node’s capacity minus what the cloud provider reserves for the kubelet and the OS: around 3.85–3.92 vCPU for a 4 vCPU node on AWS, GCP and Azure.
- DaemonSets run on every node: CNI, kube-proxy, CSI drivers, log and monitoring agents, security agents. A bare AWS node requests about 150m (
aws-node+kube-proxy); managed monitoring or security agents can add a few hundred more. - Sidecars injected in the
runtimepod (for instance an Istio proxy, typically 100m) count in the pod’s own request.
runtime request and maxWorkers together (see CPU per worker), or move to 8 vCPU nodes.
Node.js workers heap is auto-sized
Each worker thread has its own heap memory hard limit, configured via Node.js’s--max-old-space-size option. You don’t need to set NODE_OPTIONS manually; the chart auto-derives each worker’s heap from resources.limits.memory and maxWorkers:
Resource Quotas and Limit Ranges
Apply quotas at the namespace level to prevent runaway consumption. The example below fits the Balancedcore namespace at HPA max (~13 vCPU requests and ~16 GiB memory limits, or ~16 vCPU during a rollout). Size yours on your own HPA max, plus one extra pod per deployment for rolling updates: a quota that’s too tight blocks scale-outs and rollouts. The Performance core namespace, for instance, requests ~18.5 vCPU at HPA min and ~46 vCPU / ~30 GiB of memory limits at HPA max, so it needs a requests.cpu of about "56" once rollouts are counted.
Troubleshooting
Pods stuck Pending
Pods stuck Pending
- “0/N nodes available: Insufficient cpu/memory” → reduce requests, increase node size, or scale the node pool.
runtime(or another large pod)Pendingon every node, even an empty one → its request exceeds what a node can offer after DaemonSets; see Fitting the runtime on a node.- “node(s) didn’t match Pod’s node affinity/selector” → check
tolerationsandnodeSelectoragainst your node pool labels.
HPA pinned at maxReplicas
HPA pinned at maxReplicas
- Sustained 100% CPU on max replicas → raise
maxReplicas, or move from Balanced to Performance variant for the affected service. - Memory-bound service (e.g.
functions) → raise memory request rather than replica count.
Pod OOMKilled
Pod OOMKilled
- For
runtime, checkBROKER_EMIT_MAXLEN; very large events can spike memory. - For
functions, checkNODE_OPTIONS=--max-old-space-size=...matches the container memory limit. - Raise the memory request and limit together.
CPU throttling under load
CPU throttling under load
- High CPU usage approaching the request, with growing latency → raise the CPU request (not a limit).
- Verify no CPU limit is set on Prisme.ai services.
Node saturation
Node saturation
- One node carrying disproportionate load → check pod anti-affinity on critical services, or rebalance with a rolling restart.
- Persistent saturation → scale the node pool or move to larger instances.
Frequent restarts after deployments
Frequent restarts after deployments
- Failing readiness probe right after rollout → check service dependencies (DB reachable, secrets mounted).
- Crash loop →
kubectl logs <pod> -n <namespace> --previous. - If the regression is in a new chart version, roll back:
helm rollback <release> <revision> -n <namespace>; see Helm install: Upgrade, rollback, uninstall.
Volume node affinity conflict
Volume node affinity conflict
Symptom in A pod with a
kubectl get events:ReadWriteOnce PVC can only be scheduled on the node where the volume lives. If that node is full and the others can’t mount the volume, scheduling fails.Fixes:- Free CPU/memory on the node that hosts the volume (delete idle pods, scale a noisy neighbor down).
- Move to a ReadWriteMany storage class (EFS / Azure Files / Filestore / CephFS) for workloads that can be re-scheduled freely; see Requirements.
- Add a node to the pool.
PersistentVolume nearly full
PersistentVolume nearly full
Alerts:
KubePersistentVolumeFillingUp, KubePersistentVolumeFull.- For Elasticsearch or OpenSearch, the most common cause is unbounded event growth; enable the
prismeai-eventscleanup job and tune retention. See Elasticsearch: Events automated cleanup. - Expand the PVC if the storage class supports it (
kubectl edit pvc <pvc>→ bumpspec.resources.requests.storage). - Otherwise, snapshot + restore into a larger volume.
Next Steps
Helm install
Configure values and deploy core + apps namespaces.
Databases
PostgreSQL or MongoDB, Redis, Elasticsearch or OpenSearch.
Install products
Configure your Prisme.ai AI products.
Operations
Scaling, updates and backups.