<@ULVA73B9P> I'm getting real pour postgres perfor...
# ask-marvin
b
@Marvin I'm getting real pour postgres performance on one of your querries, resulting in a lot of flows and tasks failing, please suggest a few ways to improve my prefect setup on self hosted with redis and postgres? The following is the querry: SELECT with_latest_labels.value, with_latest_labels.label, sum ( with_latest_labels.count ), min ( with_latest_labels.oldest ), max ( with_latest_labels.latest ) FROM ( SELECT fundamental_counts.value, first_value ( fundamental_counts.label ) OVER ( PARTITION BY fundamental_counts.value ORDER BY fundamental_counts.latest DESC ), fundamental_counts.latest, fundamental_counts.oldest, fundamental_counts.count FROM ( SELECT events.event, events.event, max ( events.occurred ), min ( events.occurred ), count ( * ) FROM events WHERE events.occurred >= ? :: TIMESTAMP WITH TIME ZONE AND events.occurred <= ? :: TIMESTAMP WITH TIME ZONE GROUP BY value, label ) ) GROUP BY with_latest_labels.value, with_latest_labels.label ORDER BY count DESC, with_latest_labels.label ASC
m
thought for 121.7 seconds
Thanks for flagging this — that query shape can get expensive on a large
events
table and will definitely ripple into flow/task failures if the API is waiting on it. Assuming you’re on Prefect 3.x and self‑hosting Postgres + Redis, here are practical steps (DB, query, and Prefect-level) to reduce load and improve performance. I’ll also ask a few quick questions at the end so we can tailor this further. High‑impact first steps - Add the right indexes on events - You filter on
occurred
and aggregate by
value, label
, then need the latest label per
value
. Commonly helpful:
Copy code
CREATE INDEX IF NOT EXISTS events_occurred_idx ON events (occurred);
    CREATE INDEX IF NOT EXISTS events_value_occurred_desc_idx ON events (value, occurred DESC);
    CREATE INDEX IF NOT EXISTS events_value_label_idx ON events (value, label);
- If
value
has high cardinality and you often query recent time windows, the
(value, occurred DESC)
index is especially useful for “latest per value” lookups. - Rewrite the query to avoid window functions - Your query uses
first_value(...) over (partition by value order by latest desc)
which is elegant but can be expensive. You can split into clear CTEs: pre-aggregate by label, compute totals per value, and compute the latest label per value using
DISTINCT ON
against the raw events or against the aggregation. This is logically equivalent to your current result (one row per value with the latest label and total counts).
Copy code
WITH by_label AS (
      SELECT
        e.value,
        e.label,
        COUNT(*) AS cnt,
        MIN(e.occurred) AS oldest,
        MAX(e.occurred) AS latest
      FROM events e
      WHERE e.occurred BETWEEN $1 AND $2
      GROUP BY e.value, e.label
    ),
    totals AS (
      SELECT
        value,
        SUM(cnt) AS total_cnt,
        MIN(oldest) AS min_oldest,
        MAX(latest) AS max_latest
      FROM by_label
      GROUP BY value
    ),
    latest_label AS (
      -- Option A: latest event per value directly from events
      SELECT DISTINCT ON (e.value)
        e.value, e.label
      FROM events e
      WHERE e.occurred BETWEEN $1 AND $2
      ORDER BY e.value, e.occurred DESC
    )
    SELECT
      t.value,
      l.label,
      t.total_cnt,
      t.min_oldest,
      t.max_latest
    FROM totals t
    JOIN latest_label l USING (value)
    ORDER BY t.total_cnt DESC, l.label ASC;
- This shape generally plans better, makes index usage more predictable, and removes the window function. - Reduce the data you scan - If the UI/dashboard is hitting very wide time windows, tighten the default window and allow “load more” progressively. - Consider materialized rollups if this is a frequent dashboard query: - e.g., materialize hourly/day aggregates and query the materialized view, refreshing on a schedule. Control table growth - Retention/TTL: If you don’t need long-term events, prune aggressively (e.g., keep 7–30 days). This alone dramatically improves Postgres performance at scale. - Example scheduled job:
Copy code
DELETE FROM events WHERE occurred < now() - interval '30 days';
    VACUUM (ANALYZE) events;
- Ensure autovacuum is keeping up — large, append‑heavy tables need aggressive autovacuum settings. - Partitioning by time: Partition
events
by RANGE on
occurred
(e.g., daily or weekly partitions) - Helps both pruning and autovacuum; queries with
occurred BETWEEN ...
prune partitions quickly. - Useful primer: Database partitioning in production Postgres without downtime (Prefect blog) - TimescaleDB (optional): If you’re comfortable enabling Timescale as a Postgres extension, turning
events
into a hypertable gives you automatic chunking/pruning and good time‑series performance. Postgres tuning checklist - Enable pg_stat_statements and capture EXPLAIN ANALYZE for the slow query to guide index choices.
- Ensure adequate memory and I/O: - shared_buffers ~ 25% of RAM (rule of thumb), work_mem sufficient for your GROUP BY/aggregations, effective_cache_size aligned with OS cache. - Increase CPU/IOPS if you’re constrained — aggregates on large tables are CPU/IO bound. - Consider disabling JIT if you see JIT overhead for many similar short queries:
Copy code
SET jit = off;
- Use pgbouncer (transaction pooling) between Prefect API and Postgres to cap connections and protect Postgres under load. Prefect-specific levers - Keep Prefect on the latest 3.x — we’ve shipped performance improvements around the API and data access over time. - Trim event volume if possible: - Reduce noisy, high-frequency custom event emission (if you’re emitting custom events). - Avoid dashboards or automations that repeatedly query very wide windows. - Right-size API concurrency: - If you’ve scaled API replicas aggressively, consider connection pool limits and use pgbouncer to avoid connection storms. - General settings reference (for locating tunables): Prefect 3 settings reference Redis notes - Ensure Redis is sized for your load and co‑located (low latency) with your API/worker processes. While Redis isn’t the source of this SQL slowness, resource pressure in Redis can amplify overall system latency. What will help me help you faster - Prefect version (e.g., 3.x.y) - Postgres version and instance size (CPU/RAM/IOPS) and whether you use pgbouncer - Row count in
events
and growth rate - Existing indexes on `events`:
Copy code
\d+ events
- EXPLAIN ANALYZE for the current query over a representative time window - Typical time window your UI/automation is querying If you can share the EXPLAIN ANALYZE plan and current indexes, I’ll suggest exact index changes or a final query rewrite tailored to your schema and data distribution.
b
@Marvin what is the recommended AWS RDS sizing for postgres and CPU size for the Prefect server containers
m
thought for 57.8 seconds
Short answer: it depends on your flow volume and concurrency, but here are solid starting points that work well for most self‑hosted Prefect 3.x installs and scale predictably. Recommended starting sizes - Small (dev/low traffic) - RDS Postgres: db.m6g.large (2 vCPU, 8 GB RAM) or db.r6g.large (2 vCPU, 16 GB RAM) on gp3 storage with at least 3–4k baseline IOPS and 100–150 MB/s throughput - Prefect API container(s): 1 replica, 1–2 vCPU, 2–3 GB RAM - Notes: Avoid t‑class for production (CPU credits). Add pgbouncer in front of RDS to cap connections. - Medium (steady prod, frequent flows) - RDS Postgres: db.r6g.xlarge (4 vCPU, 32 GB RAM) on gp3 with 6–8k baseline IOPS and ~250 MB/s - Prefect API containers: 2–3 replicas, each 1–2 vCPU and 3–4 GB RAM - Notes: Enable pgbouncer (transaction pooling), tighten events retention (e.g., 7–30 days), and add the recommended indexes on events. - Large (high concurrency/heavy events) - RDS Postgres: db.r6g.2xlarge (8 vCPU, 64 GB RAM) or r7g.2xlarge with gp3 10–16k IOPS and 500 MB/s (or io2/io2 Block Express if you need consistent high IOPS) - Prefect API containers: 3–6 replicas, each 2 vCPU and 4–6 GB RAM - Notes: Partition the events table by time or use TimescaleDB; use pgbouncer; consider materialized views for dashboards and aggressive event pruning. Why these picks - Postgres is predominantly memory and I/O bound for Prefect’s metadata and events queries; r-class (memory-optimized) instances improve cache hit rates and reduce I/O. - Graviton (g) classes provide excellent price/perf; prefer r6g/r7g where possible. - gp3 lets you provision IOPS/throughput independently of size; don’t starve the DB of IOPS. - Prefect API is CPU-light compared to the database. Start with 1–2 vCPU per replica and scale horizontally behind a load balancer as concurrency grows. Operational tips - Use pgbouncer between Prefect API and RDS; keep DB connections predictable under traffic spikes. - Monitor and scale by: - Postgres: CPU >70% sustained, buffer cache hit ratio <99%, read IOPS or read latency rising, autovacuum lagging, p95 query latency - API: p95 request latency and queue time; scale replicas before CPU caps - Keep events lean: - Add helpful indexes (examples from earlier): (occurred), (value, occurred DESC), (value, label) - Prune/partition events; consider Timescale for hypertables - Query narrower time windows for dashboards/automations If you share a few workload signals, I can tailor this precisely: - Approx flow runs per day and typical concurrent flow runs - Average events per flow run and events retention target - Current RDS instance class/storage and observed CPU/IOPS/latency - Current API replica count and container CPU/RAM limits Related resources - Prefect blog on Postgres partitioning: Database partitioning in production Postgres without downtime - Prefect 3 settings reference (for tuning and environment flags): Prefect 3 settings reference
b
@Marvin I'm still seeing a lot of 504 errors when my Flow and Task runs are trying to communicate with the prefect server which is using a background service and Redis, what are some other things I should look into this is preventing us from scalling our solution
m
thought for 215.8 seconds
504s usually mean something in the path is timing out before the Prefect API can respond. Since you’re self‑hosting with Redis and background services, here’s a focused checklist to remove the most common bottlenecks and timeouts. Triage the network first - Load balancer/ingress timeouts - AWS ALB: increase the target group idle timeout (attribute
idle_timeout.timeout_seconds
) to 120–300s. - NGINX/NGINX Ingress: set -
<http://nginx.ingress.kubernetes.io/proxy-read-timeout|nginx.ingress.kubernetes.io/proxy-read-timeout>: "300"
-
<http://nginx.ingress.kubernetes.io/proxy-send-timeout|nginx.ingress.kubernetes.io/proxy-send-timeout>: "300"
-
<http://nginx.ingress.kubernetes.io/keepalive|nginx.ingress.kubernetes.io/keepalive>: "64"
- Ensure WebSocket pass‑through (Upgrade/Connection headers) for logs/events streaming. - One API pod should run services; others API pods should not - Run a single “services” instance to avoid duplicate background loops stampeding your DB. Others should be webserver‑only. - Verify services state:
Copy code
prefect server services ls
- Start only webserver on replicas:
Copy code
prefect server start --no-services
- Start the primary with services (scheduler, late-runs, etc.):
Copy code
prefect server services start
Tune the Prefect API - Increase keep‑alive on the API server so upstream proxies don’t cut connections mid‑request:
Copy code
prefect server start --keep-alive-timeout 120
This maps to the
PREFECT_SERVER_API_KEEP_ALIVE_TIMEOUT
setting. - Scale horizontally: run 2–6 API replicas behind your LB. Keep one “services” pod as above. - Keep up to date on Prefect 3.x — we regularly ship robustness and performance improvements. Reduce slow responses (DB/Redis) - Database first aid - Put pgbouncer (transaction pooling) in front of Postgres to cap/consolidate connections. - Ensure storage isn’t IOPS/latency constrained (RDS gp3 IOPS/throughput sized for your workload). - Add helpful indexes on
events
if you haven’t yet:
Copy code
CREATE INDEX IF NOT EXISTS events_occurred_idx ON events (occurred);
    CREATE INDEX IF NOT EXISTS events_value_occurred_desc_idx ON events (value, occurred DESC);
    CREATE INDEX IF NOT EXISTS events_value_label_idx ON events (value, label);
- Prune/partition
events
aggressively (7–30 day retention, or time‑based partitions). For partitioning guidance: Database partitioning in production Postgres without downtime - Redis hygiene - Check Redis CPU/mem/network latency; keep it close (same AZ/VPC) to the API and workers. - Avoid persistence settings that introduce latency unless required; ensure
maxmemory
and eviction policy are set appropriately. Control request pressure - Concurrency limits to prevent a “thundering herd” of state updates/logs:
Copy code
prefect concurrency-limit create my-api-guard --limit 50
Start with a limit and raise gradually based on API and DB headroom. - Reduce noisy behavior: - Throttle very chatty flows/tasks (excessive logs/events). - For dashboards/automations, query narrower time windows. Kubernetes/containers - API container resources: give each API pod 1–2 vCPU and 3–4 GB RAM to start; scale replicas before maxing CPU. - Probes: ensure readiness/liveness probes aren’t too aggressive (don’t kill pods that are busy but healthy). Observability (to pinpoint the 504 cause) - ALB/ingress: check whether 504 is from LB, and the target (API) response times. - Prefect API logs: note endpoints around the time of 504s; slow endpoints typically map to DB calls. - Postgres: enable slow query log; EXPLAIN ANALYZE the worst offenders; monitor CPU, read latency, autovacuum lag. - Redis: latency monitor and connection counts. Settings reference - Full list of Prefect settings you can tune (keep‑alive, logging, etc.): Prefect 3 settings reference A few quick clarifiers so I can tailor exact steps:
- Where is the API running (Kubernetes? EC2?), and what sits in front (ALB, NGINX Ingress, something else)? - How many API replicas are you running, and are services enabled on more than one? - Do you have pgbouncer in front of Postgres? - Typical concurrent flow runs and approximate events/log volume per run? - Current ALB/Ingress timeout settings and RDS CPU/IOPS metrics?
b
@Marvin how many connections should each prefect server have to the DB?
m
thought for 153.9 seconds
Short answer: In Prefect 3.x, each server replica uses a SQLAlchemy connection pool. By default it will hold up to 5 persistent connections and allow up to 10 overflow under load, so up to 15 concurrent DB connections per server replica. Details and tuning: - Defaults per server replica - Pool size: 5 - Max overflow: 10 - Pool timeout: 30s - Pool recycle: 3600s - Environment variables to change these -
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE
-
PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW
-
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT
-
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE
Example:
Copy code
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=10
PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=20
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=30
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE=3600
Planning capacity: - Total potential connections = replicas × (pool_size + max_overflow) - Ensure your Postgres
max_connections
(or PgBouncer pool) can accommodate Prefect plus any other clients. - If you run many replicas or share the DB with other apps, consider lowering `pool_size`/`max_overflow` or fronting Postgres with PgBouncer. Docs: - Prefect 3 settings reference If you can share how many server replicas you run, your Postgres max_connections (and whether you use PgBouncer), I can suggest concrete numbers.
b
@Marvin how many connections should we plan for per worker the server is supporting?
m
thought for 74.5 seconds
Zero. In Prefect 3.x, workers do not connect to the database; only the Prefect server does. Workers talk to the server over the API, and the server’s connection pool handles all DB access. What to plan for instead: - DB connections are per server replica: roughly pool_size + max_overflow per replica (defaults are 5 + 10 = 15). - Total potential connections ≈ number_of_server_replicas × (pool_size + max_overflow). - If you scale workers/concurrency up, you may need to increase server replicas or tune the server’s DB pool to handle the additional API-driven load. Relevant docs: - Prefect 3 settings reference - Server database configurations