artur
10/22/2025, 2:58 AMSELECT
work_queue.id AS work_queue_id,
work_queue.name AS work_queue_name,
work_queue.filter AS work_queue_filter,
work_queue.description AS work_queue_description,
work_queue.is_paused AS work_queue_is_paused,
work_queue.concurrency_limit AS work_queue_concurrency_limit,
work_queue.priority AS work_queue_priority,
work_queue.last_polled AS work_queue_last_polled,
work_queue.status AS work_queue_status,
work_queue.work_pool_id AS work_queue_work_pool_id,
work_queue.created AS work_queue_created,
work_queue.updated AS work_queue_updated
FROM
work_queue
WHERE
work_queue.id IN ($1::UUID)
how can I resolve this issue permanently? we’ve tried adding more hardware but it only gives us a couple of days extra before we need another restartMarvin
10/22/2025, 2:58 AMMarvin
10/22/2025, 3:03 AMwork_queue.last_polled, the work_queue row becomes “hot” and small hiccups cascade into high latency.
Permanent fixes that usually resolve this:
A) Reduce polling pressure and hot-row contention
- Use fewer, larger workers per queue; avoid many workers polling the same queue
- Prefer 1 worker per queue (or a small pool) with higher --limit instead of many small workers
- Increase the worker poll interval and prefetch window to keep throughput but slash DB QPS
- In Prefect 3.x defaults are roughly 10s; set:
- Workers
PREFECT_WORKER_QUERY_SECONDS=30
PREFECT_WORKER_PREFETCH_SECONDS=60
- This trades a few seconds of pickup latency for much lower DB load
B) Right-size/refresh the API’s DB connection pool
- Recycle connections more aggressively (prevents long-lived connection state/caches from growing and helps Cloud SQL)
- Bump pool size a bit if you have multiple API pods and steady traffic
- On your API pods:
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE=600
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=10
PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=20
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=30
- If you still see plan/cache pathologies over time in Cloud SQL, you can cap/disable statement caches:
PREFECT_SERVER_DATABASE_STATEMENT_CACHE_SIZE=0
PREFECT_SERVER_DATABASE_PREPARED_STATEMENT_CACHE_SIZE=0
Note: this trades memory stability for a bit more CPU; only apply if you see prepared-statement/generic-plan issues in Cloud SQL
C) Spread load across queues where possible
- Don’t point a large worker fleet at a single queue
- If you need many workers (e.g., different node pools), shard deployments across multiple queues so each queue has only a few pollers
D) Tune background services only if needed
- If your workload is very spiky, slightly increasing service loop intervals can help reduce DB churn:
- Scheduler and late-runs loops can be nudged up a bit (e.g., +5–10s) to reduce steady-state pressure without affecting behavior materially
E) Postgres/Cloud SQL hygiene
- Ensure autovacuum is keeping up on hot tables (work_queue, flow_run, task_run, logs)
- Watch bloat and vacuum lag; run VACUUM ANALYZE or pg_repack during low traffic if needed
- Verify useful indexes exist; Prefect migrations add the important ones, but confirm you have:
- Primary keys on ids (you do)
- Flow run scheduling paths (e.g., by work_queue_id and next/scheduled time)
- Monitor and mitigate long-running transactions and lock waiters
- Check pg_stat_activity and pg_locks when latency creeps up
- If you observe expensive generic plans on frequently executed queries, Cloud SQL can benefit from forcing custom plans:
- plan_cache_mode = force_custom_plan (instance-level change; test first)
Quick Kubernetes env examples
- Workers (Deployment/StatefulSet env):
- name: PREFECT_WORKER_QUERY_SECONDS
value: "30"
- name: PREFECT_WORKER_PREFETCH_SECONDS
value: "60"
- API server pods:
- name: PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE
value: "600"
- name: PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE
value: "10"
- name: PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW
value: "20"
- name: PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT
value: "30"
# Only if you confirm statement cache issues:
- name: PREFECT_SERVER_DATABASE_STATEMENT_CACHE_SIZE
value: "0"
- name: PREFECT_SERVER_DATABASE_PREPARED_STATEMENT_CACHE_SIZE
value: "0"
Why this worksMarvin
10/22/2025, 3:03 AMwork_queue table and far fewer last_polled writes (removes hot-row contention).
- Prefetch widens the pickup window so runs still start on time with less polling.
- Connection recycling avoids memory/cache buildup in long-lived connections (a known source of “everything is fine for days, then it falls over” in Cloud SQL).
- Proper pool sizing prevents thundering herds of new connections when latency spikes.
If you want, we can help tune to your footprint. A few quick questions:
- How many workers do you run per queue? How many total queues?
- What are your current PREFECT_WORKER_QUERY_SECONDS and PREFECT_WORKER_PREFETCH_SECONDS?
- How many API pods are you running, and what are their current DB pool settings?
- Typical scheduled runs per minute? Any spikes?
- Cloud SQL instance size and autovacuum settings?
Background reading if helpful:
- More memory, more problems: lessons from scaling Postgres-backed services on Cloud SQL
Once we have the numbers above, I can suggest concrete values for your environment and a rollout plan to make this stick.