- Throughput reference numbers
- Bottlenecks
- Tuning knobs
- Parallel vs serial operations
- Capacity planning rules of thumb
- Observability for performance
All numbers are estimates from a reference deployment:
- 2 API pods × 4 vCPU × 8 GB RAM
- 2 worker pods × 4 vCPU × 8 GB RAM,
CONCURRENCY=4 - Postgres db.t3.large (2 vCPU, 8 GB RAM)
- S3 object storage
- Anthropic claude-sonnet-4-6 (grounding, generation, critic)
- Anthropic claude-haiku-4-5 (explainer, describe)
Caveats: Numbers vary with file size, column count, LLM latency, and network conditions. Treat these as order-of-magnitude starting points, not SLAs.
| Operation | Typical latency | Throughput (reference) |
|---|---|---|
| Upload (10 MB CSV) | 2–5 s (upload + ingest start) | ~20 concurrent |
| Ingest — stages 1–3 (10 MB CSV) | 5–15 s | 8 concurrent (2 workers × 4) |
| Ingest — full 10-stage (10 MB CSV) | 30–90 s | 8 concurrent |
| Ingest — 100 MB Parquet | 60–180 s | 4 concurrent (memory pressure) |
| Query (simple SELECT + SUM) | 1.5–4 s (Grounding + Generation + DuckDB + Explainer) | 10–20 rps |
| Query (complex multi-join, 3 candidates) | 4–10 s | 5–10 rps |
| Query (SEMANTIC_LAYER path) | 1–2.5 s (MetricFlow deterministic) | 20+ rps |
| Embedding batch (50 columns) | 2–5 s | Limited by OpenAI rate limit |
For workspaces with large datasets (10,000+ columns), pgvector HNSW lookups
and BM25 content_tsv searches become the dominant retrieval latency.
Signals:
FLYQUERY_TOP_K_SCHEMAqueries taking > 500 ms- Slow
explain analyzeonSELECT … FROM flyquery_schema_objects WHERE embedding <-> $1 < 0.5
Mitigations:
- Tune HNSW index
ef_constructionandmparameters for your dataset size. - Add an index on
(workspace_id, is_active, kind)for the filtered scan. - Consider read replicas for retrieval queries (API) while worker writes go to primary.
Each DuckDB connection uses up to FLYQUERY_DUCKDB_MEMORY_LIMIT (default 4 GB)
for large aggregations. With 10 concurrent queries:
- Worst case: 40 GB memory on the API pod.
- In practice: most queries use < 500 MB; 4 GB is a ceiling.
Signals: Out of memory errors in query responses.
Mitigations:
- Reduce
FLYQUERY_DUCKDB_MEMORY_LIMITper connection. - Scale API pods horizontally (each pod runs its own DuckDB instances).
- Add WHERE clause pushdown in the query prompt to reduce scan sizes.
Grounding + Generation + Critic + Explainer calls run sequentially per query. Each call adds 500 ms–3 s depending on model and prompt size.
LLM call sequence latency:
- Grounding: 0.5–2 s
- Generation (N=3 candidates): 1–3 s
- Critic (if needed): 0.5–2 s
- Explainer: 0.3–1 s
- Total: 2.3–8 s (serial)
Signals: p99 query latency > 10 s.
Mitigations:
- Use
FLYQUERY_EXPLAINER_MODEL=anthropic:claude-haiku-4-5(already default). - Reduce
FLYQUERY_GENERATION_CANDIDATESfrom 3 to 1 for latency-sensitive workloads (less quality). - Enable
FLYQUERY_QUERY_EXPANSION_ENABLED=false(already default). - Use the
SEMANTIC_LAYERpath for frequently-asked metric questions (bypasses GenerationAgent).
Stage 2 (parse) and DuckDB query both read from object storage. For 1 GB Parquet files:
- Ingest stage 2: 5–15 s (streaming read + re-materialise)
- DuckDB query: 2–10 s (httpfs streaming with predicate pushdown)
Mitigations:
- Enable
FLYQUERY_DUCKDB_HTTPFS_METADATA_CACHE_MB=512(already default). - Place flyquery and object storage in the same AWS region / GCP region.
- Use Parquet with efficient row groups and column-level statistics for better predicate pushdown.
| Variable | Default | Latency impact | Quality impact |
|---|---|---|---|
FLYQUERY_GENERATION_CANDIDATES |
3 |
+50-100% per extra candidate | Higher quality |
FLYQUERY_MAX_REFINE_RETRIES |
2 |
+1-3 s per retry | Better on complex SQL |
FLYQUERY_EXPAND_ITERS |
2 |
+1-2 s per iteration | Better grounding |
FLYQUERY_TOP_K_SCHEMA |
12 |
+10-20 ms per extra row | More context for Grounding |
FLYQUERY_RERANKER_TOP_N |
30 |
+50-200 ms | Better schema linking |
FLYQUERY_DUCKDB_MEMORY_LIMIT |
4GB |
N/A | Prevents OOM |
FLYQUERY_DEFAULT_STATEMENT_TIMEOUT_MS |
30000 |
N/A | Prevents runaway queries |
| Variable | Default | Throughput impact |
|---|---|---|
FLYQUERY_INGEST_WORKER_CONCURRENCY |
4 |
Linear with job parallelism |
FLYQUERY_DESCRIBE_BATCH |
20 |
Fewer LLM calls per run = higher throughput |
FLYQUERY_DESCRIBE_BUDGET_CENTS_PER_RUN |
200 |
Budget cap; raise for faster full-describe |
FLYQUERY_SAMPLE_N |
8 |
Fewer samples = faster stage 4 |
FLYQUERY_PROFILE_ROW_THRESHOLD |
10000000 |
Raise to skip profiling on medium tables |
| Variable | Default | Effect |
|---|---|---|
FLYQUERY_DUCKDB_HTTPFS_METADATA_CACHE_MB |
512 |
Higher = fewer object-store HEAD calls |
FLYQUERY_OBJECT_STORE_PRESIGN_TTL_S |
86400 |
Reduce to limit exposure window |
FLYQUERY_RESULT_TTL_HOURS |
24 |
Reduce to save object-storage costs |
All four query agents run serially (Grounding → Generation → Critic → Explainer). This is intentional: each stage consumes the output of the previous one. No parallelisation opportunity within a single query.
Multiple queries (different requests) run concurrently via async/await. DuckDB calls run in a thread pool to avoid blocking the event loop.
Stages 1–10 are serial within a single job (each stage depends on the previous). However:
- Multiple jobs run concurrently across worker goroutines.
- Stage 6 (relation discovery) runs heuristic and
RelationProposerAgentconcurrently within the stage (two separate async tasks). - Stage 9 (embed + index) embeds all columns for a table concurrently (batch embedding API call).
Stage 9 sends all columns for a snapshot in a single batch API call
(up to FLYQUERY_DESCRIBE_BATCH=20 items per call). For large tables (100+
columns), multiple batch calls run sequentially to stay within token limits.
- Parquet snapshot size ≈ original CSV / 3–6 (compression ratio).
- Query results TTL = 24 h; average result size = 50–500 KB (preview JSON + Parquet). For 100 queries/day: 5–50 MB/day.
flyquery_schema_objectsrow size ≈ 2–5 KB per column (including embedding vector at 1536 dimensions × 4 bytes = 6 KB). For 10,000 columns: 80–110 MB in Postgres.
| Component | Memory estimate |
|---|---|
| API pod | 2–4 GB base + N_concurrent_queries × DUCKDB_MEMORY_LIMIT |
| Worker pod | 2–4 GB base + DuckDB for ingestion stages (< 1 GB typical) |
| pgvector HNSW index | ~50 MB per 10,000 vectors at 1536 dims |
DuckDB aggregations are CPU-bound. One DuckDB query can spike a CPU core. With 10 concurrent queries, expect 10× CPU cores to avoid queuing.
Each query downloads the relevant Parquet columns from object storage.
With httpfs and column predicate pushdown, only the queried columns are
transferred. For a 100-column table with 1M rows querying 3 columns:
typical transfer = 5–50 MB per query.
Total inflight ingest jobs across the fleet:
total_inflight = N_processes × FLYQUERY_INGEST_WORKER_CONCURRENCY
Each IngestWorker process bounds simultaneous handler tasks with an
asyncio.Semaphore(_CONCURRENCY) (see
concurrency.md § 4). Adding
processes is linear in capacity; adding _CONCURRENCY per process is
cheaper but capped by the per-process Postgres pool + per-key LLM RPM
budget.
Tuning rules of thumb:
| Constraint | Symptom | Knob |
|---|---|---|
| LLM provider rate limit (RPM) | Burst errors from Anthropic / OpenAI | Lower _CONCURRENCY per process, add more processes (smaller bursts per key). |
| Postgres connection pool saturation | TimeoutError on await session.begin() |
Raise pool size to ≥ N × _CONCURRENCY + API_pool connections OR run PgBouncer in transaction mode. |
| DuckDB OOM in ingest stages | Out of memory errors mid-pipeline |
Raise pod RAM OR lower _CONCURRENCY per process. |
| Pending queue grows (slow consumer) | ingest_job_count_pending from GET /api/v1/stats climbs |
Add N_processes. |
| Pending queue grows (ingest pipeline LLM-bound) | Per-job duration > 60s, queue grows | Raise _CONCURRENCY per process (more parallel work) if Postgres has slack. |
The RetentionWorker doesn't scale — one process per cluster is enough. Two processes are safe (every operation is idempotent + atomic) but redundant. See workers.md for the full deployment topology.
flyquery exposes metrics via Prometheus at /actuator/metrics:
| Metric | What it measures |
|---|---|
flyquery_query_latency_seconds |
Query pipeline latency (p50, p95, p99) |
flyquery_ingest_stage_latency_seconds |
Per-stage ingestion latency |
flyquery_duckdb_memory_bytes |
DuckDB memory usage per active connection |
flyquery_agent_tokens_used |
LLM tokens consumed (by agent name) |
flyquery_cost_cents_total |
Cumulative LLM cost |
flyquery_ingest_jobs_active |
Currently running ingest jobs |
flyquery_ingest_jobs_queued |
Jobs in PENDING state |
flyquery_schema_objects_total |
Total schema objects in KB |
flyquery_null_embeddings_total |
Schema objects with NULL embedding (alert on > 0) |
Key alerts:
flyquery_ingest_jobs_queued > 20for > 5 minutes → scale up workers.flyquery_null_embeddings_total > 0for > 10 minutes → embedding stage failed.flyquery_query_latency_seconds{quantile="0.99"} > 15→ LLM latency degradation.flyquery_cost_cents_totalrate spike → runaway describe budget or unusual query volume.
See operations-runbook.md for the full observability setup and daily health check procedures.