Bart Shappee
10/08/2025, 4:32 PMMarvin
10/08/2025, 4:32 PMMarvin
10/08/2025, 4:34 PMevents 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:
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).
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:
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.Marvin
10/08/2025, 4:34 PMSET 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`:
\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.Bart Shappee
10/08/2025, 8:50 PMMarvin
10/08/2025, 8:50 PMMarvin
10/08/2025, 8:51 PMBart Shappee
10/08/2025, 8:55 PMMarvin
10/08/2025, 8:55 PMMarvin
10/08/2025, 8:59 PMidle_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:
prefect server services ls
- Start only webserver on replicas:
prefect server start --no-services
- Start the primary with services (scheduler, late-runs, etc.):
prefect server services start
Tune the Prefect API
- Increase keep‑alive on the API server so upstream proxies don’t cut connections mid‑request:
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:
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:
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:Marvin
10/08/2025, 8:59 PMBart Shappee
10/10/2025, 12:41 AMMarvin
10/10/2025, 12:41 AMMarvin
10/10/2025, 12:44 AMPREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE
- PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW
- PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT
- PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_RECYCLE
Example:
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.Bart Shappee
10/10/2025, 12:54 AMMarvin
10/10/2025, 12:54 AMMarvin
10/10/2025, 12:55 AM