Hendrik
03/03/2026, 3:24 PMMarvin
03/03/2026, 3:25 PMMarvin
03/03/2026, 3:29 PMserver:
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
backgroundServices:
runAsSeparateDeployment: true
resources:
requests:
cpu: "500m"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
worker:
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "2Gi"
If your chart nests these sections differently, share your values.yaml and I’ll map them correctly.
Immediate mitigations to stop the OOM cycle while we diagnose
- Temporarily increase memory limits (especially for background services) to keep pods stable while we test.
- Reduce logs sent to the API to see if memory stabilizes:
- Set env PREFECT_LOGGING_TO_API=false (and/or raise PREFECT_LOGGING_LEVEL=WARNING) on workers and/or server to cut log volume. This is a quick way to check if log batching/ingestion is the driver.
- If you’re self-hosting Postgres, ensure vacuum/analyze is running regularly; very large tables (especially events/logs/runs) can cause server-side memory pressure when background services query/process them.
What typically drives steady memory growth over days
- Background services handling lots of events/logs/scheduling work (especially when separated, they’ll show the growth more clearly)
- Very verbose logs sent to the API (workers and flows producing lots of logs that the server ingests/persists)
- Database size/maintenance (heavy churn on runs/logs/events over time without adequate vacuum can amplify memory use during queries/batching)
How we can isolate the culprit quickly
- Compare memory curves by deployment:
- If server and background services both grow, start by reducing logs-to-API and checking DB size.
- If workers grow but server is steady, try disabling logs-to-API just on workers and watch memory for 24–48h.
- Share a 24–72h graph of memory usage per deployment and rough run/log volume so we can correlate.
- Confirm your Prefect and Helm chart versions; upgrading to the latest 3.x often includes stability improvements.
Next steps
- If you share your Helm chart version and values.yaml (redact secrets), I’ll:
- Verify the correct resource keys for your chart
- Suggest specific env toggles (and where to put them) to reduce log/event pressure
- Recommend HPA settings if appropriateMarvin
03/03/2026, 3:29 PMHendrik
03/03/2026, 3:58 PMMarvin
03/03/2026, 3:58 PMMarvin
03/03/2026, 4:01 PMhelm get values <release-name> -n <namespace>
or at least the relevant sections for:
- server.resources, server.env
- backgroundServices.runAsSeparateDeployment, backgroundServices.resources, backgroundServices.env
- worker.resources, worker.env
- Images/tags actually running
- The screenshot helps; a text paste is even better (redact registry if needed). For example:
prefecthq/prefect:<tag> # for server
prefecthq/prefect:<tag> # for background services (if separate)
prefecthq/prefect:<tag> # for worker
- Which pods are growing
- Over 24–72h, which of these show monotonic memory growth: server, background-services, worker?
- Any HPA on these deployments?
- Recent OOMKilled events from:
kubectl describe pod <pod-name> -n <namespace>
- Workload characteristics
- Approx runs/day and peak concurrency
- Log verbosity (INFO/DEBUG?) and whether you’re sending logs to the API
- Any high-volume event usage (automations, event-driven triggers)
- Database
- Postgres version and where it’s hosted (cloud-managed vs self-managed)
- Rough DB size and top tables (if you can, share the result of):
-- Top 5 largest tables
SELECT relname AS table, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 5;
- Any custom retention settings you’ve applied
Quick checks you can run now (optional, helps triage)
- A 24–48h test lowering log volume from flows/workers (e.g., set logging to WARNING) to see if pod memory stabilizes
- If background services are separate, watch just that deployment’s memory — it often highlights events/log ingestion pressure
References
- Prefect Helm charts
- Prefect + Kubernetes deployment guide
Once I have the values/env and which pod is growing, I’ll propose exact resource requests/limits and any env toggles that are appropriate for your setup.Hendrik
03/03/2026, 4:09 PMbackgroundServices:
...
resources:
limits:
cpu: '1'
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
revisionHistoryLimit: 10
runAsSeparateDeployment: true
...
global:
prefect:
env: []
image:
prefectTag: 3.6.2-python3.11
pullPolicy: IfNotPresent
pullSecrets: []
repository: prefecthq/prefect
cattle:
systemProjectId: p-h2hmp
httproute:
annotations: {}
enabled: true
extraRules: []
hostnames: []
labels: {}
name: ''
parentRefs:
- name: ''
namespace: ''
port: null
sectionName: https
path: /
tls:
redirect: false
redirectPort: 443
ingress:
annotations: {}
className: ''
enabled: false
extraHosts: []
extraPaths: []
extraRules: []
extraTls: []
host:
hostname: prefect.local
path: /
pathType: ImplementationSpecific
selfSigned: false
servicePort: server-svc-port
tls: false
migrations:
affinity: {}
backoffLimit: 5
command: |
prefect server database upgrade -y
enabled: true
entrypoint:
- /bin/sh
- '-c'
env: []
extraVolumeMounts: []
extraVolumes: []
nodeSelector:
prefect: enabled
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
restartPolicy: Never
securityContext:
allowPrivilegeEscalation: false
capabilities: {}
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
timeoutSeconds: 300
tolerations:
- effect: NoSchedule
key: bm-worker
operator: Exists
nameOverride: ''
namespaceOverride: ''
postgresql:
auth:
database: null
enablePostgresUser: null
password: null
username: null
existingSecret: prefect-server
enabled: false
image:
repository: null
tag: null
primary:
initdb:
user: null
persistence:
enabled: null
redis:
architecture: null
enabled: false
image:
repository: null
tag: null
secret:
create: null
database: null
host: null
name: null
password: null
port: null
username: null
server:
affinity: {}
apiBasePath: /api
args: []
autoscaling:
enabled: false
maxReplicas: 100
minReplicas: 1
targetCPU: 80
targetMemory: 80
basicAuth:
authString: null
enabled: true
existingSecret: prefect-server
command: []
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities: {}
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
debug: false
env:
- name: PREFECT_EVENTS_RETENTION_PERIOD
value: 3d
- name: PREFECT_API_DATABASE_TIMEOUT
value: '1800'
extraArgs: []
extraContainers: []
extraEnvVarsCM: ''
extraEnvVarsSecret: ''
extraVolumeMounts: []
extraVolumes: []
livenessProbe:
config:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
enabled: false
loggingLevel: WARNING
nodeSelector:
prefect: enabled
podAnnotations: {}
podLabels: {}
podSecurityContext:
fsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seccompProfile:
type: RuntimeDefault
priorityClassName: ''
readinessProbe:
config:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
enabled: false
replicaCount: 1
resources:
limits:
cpu: '1'
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
revisionHistoryLimit: 10
tolerations:
- effect: NoSchedule
key: bm-worker
operator: Exists
uiConfig:
prefectUiApiUrl: <http://localhost:4200/api>
prefectUiStaticDirectory: /ui_build
updateStrategy:
type: RollingUpdate
service:
annotations: {}
clusterIP: ''
externalTrafficPolicy: Cluster
extraPorts: []
nodePort: ''
port: 4200
targetPort: 4200
type: ClusterIP
serviceAccount:
annotations: {}
create: true
name: ''
sqlite:
enabled: false
persistence:
enabled: null
size: null
storageClassName: nullMarvin
03/03/2026, 4:09 PMMarvin
03/03/2026, 4:09 PMMarvin
03/03/2026, 4:10 PMMarvin
03/03/2026, 4:10 PMMarvin
03/03/2026, 4:11 PMMarvin
03/03/2026, 4:12 PMserver:
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
backgroundServices:
runAsSeparateDeployment: true
resources:
requests:
cpu: "500m"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
For workers (please share your current values), a good starting point is:
worker:
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "2Gi"
2) Cap the in-memory event persister queue
Add to the server (and background services if you set env separately on that deployment):
server:
env:
- name: PREFECT_SERVER_SERVICES_EVENT_PERSISTER_QUEUE_MAX_SIZE
value: "20000" # default is higher; 20k is safer in constrained clusters
- name: PREFECT_SERVER_SERVICES_EVENT_PERSISTER_BATCH_SIZE
value: "20" # keep defaults unless you see backpressure
- name: PREFECT_SERVER_SERVICES_EVENT_PERSISTER_FLUSH_INTERVAL
value: "5"
This prevents unbounded in-memory growth when event volume spikes.
3) Keep event retention short while tuning
You already have:
- name: PREFECT_EVENTS_RETENTION_PERIOD
value: 3d
That’s a good start. If your throughput is very high, try 1–2d temporarily while we stabilize memory.
4) Reduce log pressure from workers/flows (quick isolation step)
- Temporarily raise logging to WARNING to see if memory stabilizes:
worker:
env:
- name: PREFECT_LOGGING_LEVEL
value: WARNING
If you’re sending a lot of logs to the API, this commonly shows up as slow-but-steady growth in background services.
5) Check Postgres table sizes
Large events/logs/run tables amplify memory during background processing. If you can, run:
-- Top 5 largest tables
SELECT relname AS table, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 5;
If events/logs are dominating, retention + the queue cap above almost always helps.
Optional: upgrade to latest 3.x
3.6.2 is solid, but if you can bump to the latest 3.x patch you’ll pick up stability fixes. See Prefect releases.
If you share your worker values (and which pod grows the fastest), I’ll tailor the exact env/resource changes for that deployment and suggest any additional toggles specific to your workload.
References
- Helm charts: Prefect Helm charts
- Settings reference (env vars): Prefect settingsHendrik
03/03/2026, 4:14 PMcommonAnnotations: {}
commonLabels: {}
fullnameOverride: prefect-worker
nameOverride: ''
namespaceOverride: ''
role:
create: true
extraPermissions:
- apiGroups:
- ''
resources:
- pods
- pods/log
- pods/status
verbs:
- get
- watch
- list
- apiGroups:
- batch
resources:
- jobs
- events
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
namespace: null
rolebinding:
create: true
serviceAccount:
annotations: {}
create: true
name: ''
worker:
affinity: {}
apiConfig: selfHostedServer
args: []
autoscaling:
enabled: false
maxReplicas: 1
minReplicas: 1
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
cloudApiConfig:
accountId: ''
apiKeySecret:
key: key
name: prefect-api-key
cloudUrl: <https://api.prefect.cloud/api>
workspaceId: ''
clusterUid: ''
command: []
config:
baseJobTemplate:
configuration: null
existingConfigMapName: base-job-template
http2: true
installPolicy: prompt
jobNamespace: null
limit: null
name: null
prefetchSeconds: 10
queryInterval: 5
type: kubernetes
workPool: prod-worker-1
workQueues: []
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities: {}
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
dnsConfig:
nameservers: []
options: []
searches: []
dnsPolicy: ''
extraArgs: []
extraContainers: []
extraEnvVars: []
extraEnvVarsCM: ''
extraEnvVarsCMs: []
extraEnvVarsSecret: ''
extraEnvVarsSecrets: []
extraVolumeMounts: []
extraVolumes: []
image:
debug: false
prefectTag: 3.6.2-python3.11-kubernetes
pullPolicy: IfNotPresent
pullSecrets: []
repository: prefecthq/prefect
initContainer:
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities: {}
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
extraContainers: []
resources: {}
livenessProbe:
config:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
enabled: false
nodeSelector:
prefect: enabled
podAnnotations: {}
podDisruptionBudget: {}
podLabels: {}
podSecurityContext:
fsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seccompProfile:
type: RuntimeDefault
priorityClassName: ''
replicaCount: 1
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 100m
memory: 256Mi
revisionHistoryLimit: 10
selfHostedServerApiConfig:
apiUrl: <http://prefect-server.prefect.svc.cluster.local:4200/api>
basicAuth:
authString: null
enabled: true
existingSecret: prefect-server
selfManagedCloudApiConfig:
accountId: ''
apiKeySecret:
key: key
name: prefect-api-key
apiUrl: ''
cloudApiUrl: ''
workspaceId: ''
tolerations:
- effect: NoSchedule
key: bm-worker
operator: Exists
global:
cattle:
systemProjectId: p-h2hmp
this is my prefect worker deployment yaml @MarvinMarvin
03/03/2026, 4:14 PMMarvin
03/03/2026, 4:20 PMserver:
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
backgroundServices:
runAsSeparateDeployment: true
resources:
requests:
cpu: "500m"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
2) Apply retention and cap the in-memory events queue on background services
Add these env vars to background services so they actually take effect there:
backgroundServices:
env:
- name: PREFECT_EVENTS_RETENTION_PERIOD
value: "3d" # move/copy from server.env so background services see it
- name: PREFECT_SERVER_SERVICES_EVENT_PERSISTER_QUEUE_MAX_SIZE
value: "20000" # cap the in-memory buffer to prevent slow creep
# Optional fine-tuning (usually not needed unless you see backpressure):
# - name: PREFECT_SERVER_SERVICES_EVENT_PERSISTER_BATCH_SIZE
# value: "20"
# - name: PREFECT_SERVER_SERVICES_EVENT_PERSISTER_FLUSH_INTERVAL
# value: "5"
If your chart doesn’t expose backgroundServices.env, you can place these under global.prefect.env to apply cluster-wide, or let me know and I’ll map to your chart’s exact key.
3) Temporarily reduce log pressure from the worker (quick isolation step)
By default, workers send logs to the API at INFO level. To see if logs are the driver, try:
worker:
extraEnvVars:
- name: PREFECT_LOGGING_TO_API_ENABLED
value: "false"
- name: PREFECT_LOGGING_LEVEL
value: "WARNING"
If memory stabilizes over 24–48h, we can re-enable logs-to-API with smaller batches:
- name: PREFECT_LOGGING_TO_API_ENABLED
value: "true"
- name: PREFECT_LOGGING_TO_API_BATCH_SIZE
value: "2000000"
- name: PREFECT_LOGGING_TO_API_BATCH_INTERVAL
value: "5.0"
4) Worker resources
Your worker is requests: 256Mi, limits: 1Gi. That’s fine as a starting point; if you see the worker pod creeping, bump to 512Mi/2Gi:
worker:
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "2Gi"
Why these changes help
- Background services do the heavy lifting for events/log ingestion and scheduling—moving retention to that pod and capping the persister queue prevents unbounded in-memory buffers.
- Disabling or reducing logs-to-API is a fast way to test if log batching/ingestion is the memory driver.
- Raising limits stops the OOM cycle while we observe the effect of the queue cap and logging adjustments.
What I still need (to tune further)
- Which deployment OOMKills first: server, background-services, or worker?
- Approx runs/day and whether flows are verbose (INFO/DEBUG) with lots of log lines.
- If possible, a quick check of top Postgres tables (events/logs/runs) size — large tables amplify background-service memory during processing:
SELECT relname AS table, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 5;
If you apply the snippets above and share which pod was growing before/after, I’ll help dial in the final resource and env settings.
Refs
- Helm charts: Prefect Helm charts
- Settings (all env vars): Prefect settings