Operating a sharded cluster¶
What to expect when you run, scale and restart a sharded HyperbyteDB cluster on Kubernetes. Every number here was measured, and each section says where and on which build. Treat the numbers as an order of magnitude for your own hardware, not as a promise.
Test bed. Unless a section says otherwise: the n3 k3s cluster, one shared host with 24 cores (48 threads), pods limited to 3 CPU and 16 GiB, replication factor 2, local-path volumes, the in-cluster hyperbytedb-cli bench (2 clients per node, 64 requests in flight each, 5,000-point batches, 400,000 series per node). The host also runs other workloads, so throughput varies from run to run by tens of percent; compare builds only side by side.
Quick reference¶
| You do | What happens | How long (n3) |
|---|---|---|
| Restart every pod (Parallel policy) | All pods restart together; writes fail until a coordinator is back | ~105 s for 8 pods |
| Restart every pod (OrderedReady) | One pod at a time | over an hour for 8 pods |
| Scale up 3 → 6 | New pods ready in seconds; regions copy onto them in the background | pods 20 s, layout 48 min |
| Scale up 6 → 8 | Same | pods 18 s, layout over 60 min |
| Scale down 8 → 3 | Departing nodes go unready, their regions are copied off, then they leave | hours at current copy speed (see below) |
CREATE MATERIALIZED VIEW | Replicated through Raft to every node | ~30 ms |
No points were lost in any of these steps on build 8d7086e / 792c94b (2026-10-10): every database written during the run read back in full, about 1.02 billion points across six databases.
Versions¶
This page describes HyperbyteDB 0.9.1-beta with operator v0.3.0. The operator renders each cluster for the HyperbyteDB version it runs (see HyperbytedbCluster: annotations and versions); on 0.8.x the sharding, control-listener and evacuate-together features below are not available.
Operator annotations¶
All are opt-in, per cluster, and set on the HyperbytedbCluster object.
| Annotation | Effect | Recommended |
|---|---|---|
hyperbytedb.hyperbyte.cloud/parallel-pods: "true" | The StatefulSet uses Parallel pod management. The operator recreates the StatefulSet once to apply it (pods are kept). | Yes. Under OrderedReady a departing node reports unready, and Kubernetes will not create a pod above an unready one: a pod deleted during a scale-down was never recreated, and a full restart took over an hour. |
hyperbytedb.hyperbyte.cloud/evacuate-together: "true" | A scale-down decommissions all departing nodes at once instead of one at a time. | Yes, on 8d7086e or later (see "Scaling down"). |
hyperbytedb.hyperbyte.cloud/control-listener: "true" | Liveness and Raft traffic use a separate listener (server port + 1) on its own runtime, so a write-saturated node still answers probes and Raft. | Yes. |
Writes¶
- Throughput. The acceptance test (fresh cluster per size, 90M points, equal host load via a twin cluster, 3 runs each;
20c761f, 2026-10-11): N=3 865k points/s (860-904k), N=6 1.58M (1.51-1.60M), 1.82x, every point read back, 0 restarts, 10.6-11.8 us of CPU per point. Without the twin load, N=3 writes 1.05-1.24M points/s. - Right after a scale-up writes are close to a fresh cluster: 3 -> 6 with 90M points stored and a writer running, then N=6 wrote 1.38M points/s with 41 errors as soon as the layout settled.
- Errors under overload. When a coordinator cannot reach another region's primary in time, the write fails with
write partially applied: X of Y forwarded regions failed (local bucket applied). The part already applied is safe to write again (rows are idempotent per series and time), so clients should retry the whole batch. The bench saw this at N=6 on a busy host (6,474 errors, all retried; nothing lost). - During scaling writes keep working. A steady writer saw 3 errors in 37,411 requests during 3 → 6, and 0 errors in 52,715 requests during 8 → 3, including a restart of every pod in the middle of it.
- Acknowledged writes survive a process crash: the WAL is synchronous. On a sharded node the WAL keeps at most 1 GiB behind the flush.
Disks¶
All of a node's writes (WAL, chDB parts, merges) land on its volume, and on a shared host the disk is usually what saturates first. On n3 every volume is on one NVMe: at 6 nodes writing flat out it carries 320-340 MB/s at about a third busy. Saturated, it looks like this:
- writes refused with
write partially applied: ... operation timed outand HTTP 503 (still rejected after 12 retriesfrom a client that retries); hyperbytedb_shard_forward_capacity_retries_totalandhyperbytedb_write_admission_rejected_total{kind="shard"}climbing fast (about a million capacity retries in one 90M-point run);- the host's disk near 100% busy with write waits of 50 ms or more.
That is what an uncompressed WAL did during this release (ec65682, reverted in 20c761f): 490-540 MB/s, 69-75% busy, and N=6 writes down to 0.72-1.02M points/s. Give each node its own disk where you can, and watch disk busy % alongside CPU.
Disk space after a scale-up is not reclaimed. A node that gives a region to a new node keeps its copy of the rows (queries never read them; only the region's current peers are asked). After 3 -> 6 with about 91M points at replication factor 2 (182M rows needed), the cluster stored 300M: the three old nodes kept their full pre-scale-up copies. Size disks for the pre-scale-up data until this is fixed.
Reads¶
- A query whose
WHEREfixes every tag (host = 'a', anORof those, or a Grafana-stylehost =~ /^(a|b|c)$/, each with time bounds) goes only to the regions that hold those series. Anything else goes to every region. - Each region a query touches is one chDB query on the node that holds it, so concurrent multi-region queries compete for that node's query pool. The default is one connection per CPU the pod may use, plus one (4 at 3 CPU). Change it on a running cluster with
SET query_pool_size = <n>(every node resizes at once;SHOW SETTINGSshows the value and where it comes from;SET query_pool_size = DEFAULTgoes back). With 4 concurrent 10-hostGROUP BY hostqueries over 6 regions, p50 was 250-450 ms with a pool of 1 and 90-130 ms with a pool of 4; a single such query alone takes about 55 ms, the same as one host. - The coordinator merges every region's partial result. Before
bbadb08that merge was quadratic in rows per series: a Grafana panel (mean per 30 s by host over 12 h, 5 hosts, 10 regions) spent 746 of 804 ms merging. Onbbadb08the same query takes 70-150 ms on the UAT cluster: about 60-90 ms in the region queries and 18 ms merging. - Against InfluxDB 1.11 on the same Telegraf data (UAT, 6 nodes vs 1, ~500 series,
110ff32, p50): single-series reads 11-33 ms (InfluxDB 1-2 ms), per-host panels 18-37 ms (2-7 ms), 245-series panels 53-56 ms (177-178 ms),SHOWcalls 2-23 ms (1-2 ms). Every query touches several regions, so HyperbyteDB pays a fan-out cost InfluxDB does not; it wins once a query returns many series. Before0182f6fset TCP_NODELAY, every query that touched a remote region waited up to 40 ms more (delayed ACK). - A point lookup (
last(value)for one host) is 30-60 ms p50 under the same load. - Read bench with 90M points (4 concurrent clients, p50), at 3 nodes and then at 6 after scaling up: one-host
last72 / 46 ms, raw series 32 / 23 ms, 10-hostGROUP BY host(last) 399 / 215 ms, the Grafana regex form 207 / 110 ms, a fullcount14.5 / 9.3 s. No query errors. - A larger pool is not always faster. Each chDB query uses several threads, so on 3-CPU pods a pool of 12 was 10-40% slower than 4 for 10-host queries (p50
GROUP BY host2.0-2.2 s vs 1.5-1.6 s, 2 interleaved rounds,8d7086e). Size the pool to the CPU limit. - Cost grows with the regions a node holds. The same 10-host query took ~120 ms on a 6-region measurement and ~1.5 s on one with 48 regions over 3 nodes: each node runs one chDB query per region it holds.
- During a restart, a query that needs a node that is not up yet fails with
peer unreachable(HTTP 502) rather than returning partial results.
Regions per measurement¶
A new measurement starts as one region with replication_factor copies, and splits once a region passes region_split_series (100k). Small neighbouring regions (under region_merge_series, 20k, together) are moved onto the same nodes and merged back. A query touches every region it can not rule out, and waits for the slowest, so fewer regions is faster: on UAT a 1-region query took 11-13 ms where a 10-region one took 20-60 ms. After UAT's 130 regions collapsed to 13 (one per measurement; 7 minutes with split_merge_interval_secs = 60), per-host panels took 15-22 ms p50. A 245-series panel over 2 h took 117 ms against 53 ms on 10 regions: one region's data is read by one node, so very wide queries lose the fan-out they had (InfluxDB took 200 ms).
Before ebd92e8, bootstrap_regions_per_node defaulted to 2: every new measurement started with two regions per node, even one of 30 rows, and since regions spread across nodes by design never shared a node set, they never merged back. Set it (or SET bootstrap_regions_per_node = 2) only when a new measurement will take heavy writes at once; it spreads them from the first point instead of after the first split.
Scaling up¶
- The new pods start and join in seconds.
- The scheduler copies regions onto them in the background, a few at a time (
sharding.schedule_limit, default 4;SET schedule_limit = <n>changes it live). A copy adds the new node as a learner, copies the data, then promotes it; reads never go to a learner. - "Settled" means no copy is in flight and every node holds regions. 3 → 6 took 24 minutes with 91 million points stored and a writer running, and 48 minutes with about 280 million; 6 → 8 did not settle within an hour.
Copy speed is the main limit today: about 2.2 MB/s per copy, ~4.5 MB/s for the whole cluster with two copies running. The movement budget (sharding.movement_budget_bytes_per_sec, default 50 MiB/s) is not what limits it. 79a4eaf makes the chunk export about 4x cheaper (0.24 s → 0.06 s per 50,000 rows); the rest of the per-chunk cost is still being profiled.
Before 3934cf8 a copy could lose or never finish: it paged through a region by series id alone, so a series with more than 50,000 rows (about 6 days at 10 s) had the rest of its rows skipped, and a chunk of a wide measurement (Telegraf's cpu, 10 fields) was over the receiver's size limit and refused (413) forever, leaving its new copies unpromoted. Copies now page on (series_id, time, ingest_seq) and halve their chunks on a 413 (hyperbytedb_shard_transfer_chunk_shrunk_total).
Watch: hyperbytedb_shard_transfer_bytes (rate per pod), hyperbytedb_shard_learners_added_total and hyperbytedb_shard_learners_promoted_total, and the layout at GET /internal/shard/map (staging_peers lists learners).
Scaling down¶
- The operator marks the departing nodes
decommissioning(all at once withevacuate-together, otherwise one at a time). - Departing pods report unready (
/healthreturns 503). This is expected; they keep serving the copies of their data. - Each region on a departing node gets a copy on a remaining node, then the departing peer is removed. Its materialized view rollups are handed to a remaining node (see Materialized views). When it holds no region other than view destinations and its rollups are handed off, the node marks itself
leaving(its/healthreports it), and the operator deletes its pod and PVC.
Expect it to take as long as copying the departing nodes' data at the copy speed above.
Fixed in 8d7086e: removing a departing peer used to wait for one of the 4 mover slots that copies hold for minutes. On n3 an 8 → 3 scale-down added live copies to 80 regions but dropped almost no departing peers, so no node could leave. After the fix the departing nodes dropped about 63 peers in the first minute.
6 → 3 (no restarts, 8d7086e) finished in 35 minutes.
Avoid scaling up while a scale-down is in progress. On n3, raising replicas from 3 to 6 mid-scale-down re-admitted two departing nodes (they went leaving → syncing → active), but the node that was also the Raft leader stayed departing inside a 6-replica cluster until the next scale-down removed it. No data was lost; wait for the scale-down to finish first.
A departing node can stay Raft leader until its pod is deleted, which then triggers an election. Membership state can differ between nodes for a while: on n3 the leader showed a node as leaving while a follower's GET /cluster/nodes still showed decommissioning. Ask the leader (GET /cluster/leader names it) when the state matters.
Before 25803d7 a new measurement created during a scale-down could have its regions placed on a departing node (see Materialized views).
Watch: GET /cluster/nodes (each node's state), hyperbytedb_shard_decommission_completed_total, and the per-node peer counts in GET /internal/shard/map.
Auth¶
With spec.auth.enabled the operator keeps the cluster token and the admin login in Secret <cluster>-auth and every node gets them as env (see Authentication). On the n3 UAT cluster (6 nodes, b7aa4f4, 5 Telegraf writers, 2026-10-11):
- Membership, Raft and replication run as without auth: 6 active voters, no restarts.
- A
telegrafuser withGRANT WRITEwrites but cannot read or create users; agrafanauser withGRANT READreads but cannot write. Internal routes refuse a missing or wrong token (401); the token does not work on/write. - Grants survive restarting all six pods at once. Before
b7aa4f4they did not: a restarted node rebuilt every user without its grants, and Telegraf got 403 on every write. - A non-admin running an admin statement (
CREATE USER) gets HTTP 200 withforbidden: admin privileges requiredin the body; a refused read or write is a 403. -
While all pods restart together, writers see 503s, connection refusals and
write partially applied500s for about two minutes. Telegraf retries from its buffer; all 13 of its measurements were present afterwards (point counts across the restart were not checked). -
Changing
spec.version(or the image) rolls the pods one at a time through the StatefulSet: 6 UAT pods took 13 minutes, about 2 minutes each (mostly the old pod draining). Telegraf logged 50 retried write errors over the whole rollout: writes need the region primary, and a draining primary takes none until its regions' primacy has moved. - Reads keep working through a rollout from
23fe04b. Before it, aggregates read only from the region primary, and a draining one counted as down: Grafana showedno active peers for region 121while the pods holding that region restarted. Now a draining primary still answers, and when the primary is gone a replica does (it can lag by the replication delay). The next rollout logged no such error.
Runtime settings¶
SET <setting> = <value> changes a running cluster without a restart; SET <setting> = DEFAULT puts each node back on its own config. SHOW SETTINGS lists all 20 with their source (full table in the reference). Uses that come up:
- Debug one problem:
SET log_level = 'hyperbytedb=debug,info', thenDEFAULTwhen done. - Hold placement during an upgrade or an incident:
SET add_peer_proposals_enabled = FALSE(andremove_...). Copies in flight finish; no new ones start. - More or fewer copies at once while scaling:
SET schedule_limit = <n>. Raising it only helps if the disk has headroom (see Disks). - Shed or admit write load:
SET max_inflight_writes = <n>(per node; past it clients get 503 and retry),SET rate_limit_per_second = <n>(0turns it off).
A SET goes through Raft, so it needs a leader; it reaches every node at once and survives restarts. On n3 (6 nodes, d50523a) each SET returned in under a second and SHOW SETTINGS on every pod showed it 5 s later; a restarted pod applied all 8 stored overrides at startup; log_level debug lines appeared within 20 s and stopped after DEFAULT. No Raft election.
Restarts and upgrades¶
- With
parallel-pods, deleting every pod restarted 8 pods in about 105 s and 3 pods in 107-114 s. Writes fail while no coordinator is up. - A rolling restart under
OrderedReadytakes about 16 s per pod when every pod is ready; during a scale-down it stalls (see the annotation above). GET /cluster/leadershows the Raft leader and term. A term that keeps rising means leadership is being lost (before7c81d77, everyCREATE MATERIALIZED VIEWcost an election).
Materialized views¶
- Each row is rolled up on the node that first stored it: the primary of its region at the time it was written. That node keeps the rollup in its own copy of the destination, even after the region moves elsewhere. A query on the destination asks every node that may hold rollups (active, draining, disconnected or decommissioning) and adds the results. If one of those nodes cannot answer, the query fails instead of returning a short count.
CREATE MATERIALIZED VIEWgoes through Raft and returns in about 30 ms. A node that has not seen the source measurement yet gets its schema with the statement.hyperbytedb_schema_mutations_applied_total{op="create_materialized_view"}should rise by one on every node; a node where an apply fails logsschema mutation apply failed.WITH BACKFILLrolls up the existing history once, run by the Raft leader after its own apply; a node that has not created the view yet is retried for up to 30 s (b570f03and later; before that every node backfilled, counting the history once per node). n3, 60,000 points: 60,000 rolled up on8d7086e, 90,900 on792c94b.- Destination regions are never moved by the scheduler.
- A departing node hands its rollups to a node that stays before it leaves (
6dac99e): the leader asks it to send its destination rows to the lowest-id active node, each range inserted once even if retried, then it empties its own tables. Logs:view rollups handed offon the leader,materialized view rollups handed offon the departing node. A node that cannot hand off within 30 minutes leaves without it (view rollup handoff kept failingat error level); its share of the rollups is then lost. Before6dac99eevery scale-down lost the departing nodes' rollups: on n3 a view held 44,700 of 60,000 points after 3 -> 6 -> 3. On0082e44, a view created and written at 6 nodes (through all 6 pods) still held exactly 60,000 of 60,000 after scaling to 3; all three departing nodes handed off within a minute and the scale-down finished in 59 s. - Destination regions keep naming departed nodes. They never move, so after a scale-down
GET /internal/shard/mapcan show a destination region whose only peer is a node that has left. This has no effect on data or queries (destination queries ask every member node, not region peers). - Rows that reach a destination any other way are dropped at flush with an error log and counted in
hyperbytedb_flush_rows_dropped_total{reason="materialized_destination"}. This should stay at 0.
Verified on n3 (6d354fb): 60,000 points written through three pods rolled up to exactly 60,000 (counts and means match per host per minute), both right away and five minutes later.
Dropping a database¶
DROP DATABASE on a 90-million-point database returned in 7 s on 3 nodes, with peak memory up about 0.2 GiB per pod and no restarts (8d7086e).
Pending measurements¶
These are being measured on the beta candidate and will be added here:
- Where the rest of a region copy's time goes.