Sizing and Capacity Planning

turbopuffer gets faster the more of your data it keeps in cache. Query nodes hold namespaces on NVMe SSD and in memory, in front of object storage. Add query nodes and more of your working set stays cached, so latency drops. Run fewer and you spend less, while queries to evicted data read object storage and take longer. How much to spend is your call. The turbopuffer team can recommend a starting size and help you tune it from telemetry, and you can scale up or down at any time.

A BYOC cluster runs two kinds of turbopuffer nodes:

  • Query nodes serve queries and writes from an NVMe SSD and memory cache in front of object storage.
  • Index nodes run background jobs: indexing, WAL compaction, and other maintenance.

Each query and index pod gets a dedicated node, so the pod count is the node count. Router pods run on query nodes, and their count follows min_query_nodes. Node pools autoscale with Karpenter on AWS and Azure and the cluster autoscaler on GCP, so nodes are created and removed as pods are scheduled. You choose the pod counts. Index nodes autoscale on their own, while query nodes stay at the count you set.

Minimums and maximums

Your BYOC kit sets the node floors in turbopuffer.tfvars, and terraform apply writes them to values.yaml as turbopuffer.min_replicas:

min_query_nodes = 6
min_index_nodes = 1

There is no single right minimum. For most clusters, min_index_nodes = 1 is enough and the autoscaler adds index nodes when needed. Raise it only if you want capacity guaranteed rather than waiting for the cloud provider to supply nodes.

The maximum number of index nodes comes from the node pool limits in Terraform (10 per zone on AWS and Azure, 128 on GCP). Don't raise max_replicas in Helm alone, since pods beyond the node pool limit can't be scheduled.

Check that your cloud quota covers the maximum node count for both pools, plus any pinned nodes.

Query nodes

There is no autoscaler for query nodes, since adding and removing query nodes moves namespaces between caches and causes cache thrashing. The query StatefulSet stays at the size you set, and upgrades keep its current size. Raising min_query_nodes scales up on the next deploy. The turbopuffer team can also scale query nodes through the control plane.

Each namespace is routed to one query node so that it stays in that node's cache. Size query nodes so that:

  • The working set fits in cache. The combined NVMe cache of all query nodes should hold the namespaces you query regularly, with headroom for growth. If it doesn't, namespaces are evicted and the next query to them reads object storage and is slow.
  • CPU has headroom at peak. Plan for peak QPS, not average QPS.
  • Hot namespaces have room. A single namespace with very high QPS is limited by one node. Spread it across more nodes with ingress.read_replicas, or give it dedicated nodes with pinning.

Add query nodes when the following widgets on the BYOC Datadog dashboard trend the wrong way:

WidgetWhat to look for
NVMe cache age (query)Age dropping over time. Data is evicted sooner, so the cache is too small for the working set.
Memory cache max evicted age (query)Same signal for the memory cache.
Storage cache hit ratio (query pods)Ratio dropping, so more queries read object storage.
Total CPU usage by query host (%)Hosts close to saturation at peak. A single hot host points to a hot namespace rather than too few nodes.
Query latency over timeRising p90 and p99.
tpuf 408,429,499 responses (queries)429s and 408s as query concurrency or timeouts are hit.
Query podsReady and not ready replicas, to confirm a scale-up has finished.

Index nodes

Index nodes autoscale with a Kubernetes HPA on the turbopuffer-index-0 Deployment. The HPA scales on queue delay: the age of the oldest job waiting to be picked up, with a target of about 4 minutes. If the oldest job has waited longer than the target, the HPA adds nodes in proportion. When the queue is empty, it scales down to the number of nodes needed for jobs that are still running, and never below min_index_nodes. Index nodes don't scale to 0, since compaction, garbage collection, and recall jobs also run on them.

Scale-up adds at most 1 node per minute. Scale-down waits 3 minutes and then removes at most 3 nodes every 2 minutes. The autoscaler handles bursts such as backfills, but because it adds about 1 node per minute a large backfill takes a while to reach full capacity.

If desired index pods rarely rise above min_index_nodes, the floor is higher than your workload needs and you can lower it.

Data that hasn't been indexed yet is still searched, more slowly. If a namespace falls more than 2 GB or 1 million rows behind, writes to it return HTTP 429 until indexing catches up (see disable_backpressure). Check:

WidgetWhat to look for
Indexing Job Max Queue TimeJobs waiting well past ~4 minutes for long periods.
Unindexed bytesUnindexed data per namespace that grows instead of draining.
Indexing autoscaling thresholdValues above 1 mean the HPA wants more index nodes.
Index podsDesired and ready replicas. Desired replicas flat at the maximum mean the limit is too low, and desired replicas that never rise above the minimum mean the floor is too high.
Insufficient Capacity Errors from CloudThe cloud provider can't supply nodes of the requested type in your zones.
Total CPU usage by index host (%)Index node utilization.
Jobs waiting and Jobs runningQueue depth and running jobs by queue.