# HYDRA: When a Byte Range Stops Being a Promise

- Author: Javad Rajabzadeh
- Date: 2026-09-19
- URL: http://javad.dev/posts/hydra-range-preemption-engine/
- Tags: #rust #networking #systems #http #scheduling #performance 

---


Every download manager ever shipped asks its user two questions before a single byte has moved: how many connections should I open, and how finely should I slice the file? Both are scheduling decisions made with zero observations in hand, and both have unbounded worst-case error. Split an object evenly across $N$ connections and the makespan ratio against the achievable optimum is $\overline{\gamma}/\gamma_{\min}$ — unbounded in how heterogeneous the connections turn out to be. Split it in proportion to measured rates instead, which is the obvious repair, and it is *also* unbounded, because any division fixed at $t=0$ is wrong the moment one rate moves.

[**HYDRA**](https://github.com/ja7ad/hydra) is a multi-source download engine written in Rust. Its claim is not that it copies bytes faster than `curl` — at one connection it is statistically indistinguishable from `curl` and `aria2c`. The claim is that both of those knobs are artefacts of an assumption that does not hold over HTTP, and that once the assumption is dropped, they stop being knobs and become computed quantities.

---

## 1. The number nobody questions

Connection count gets all the attention, but chunk size is the more interesting one, because there is a real theorem underneath it.

Kruskal and Weiss (1985) studied allocating independent subtasks to parallel processors when each allocation carries a fixed overhead $\delta$. The chunk that minimises overhead plus imbalance scales as $\sqrt{\delta T_{\mathrm{rem}}}$ — the square-root rule. Carried into the time domain it prescribes a chunk duration $\tau^{\ast} = \sqrt{\delta T_{\mathrm{rem}}}$, and it implies a makespan excess over the optimum growing as $\sqrt{T^{\ast}}$.

Read that last part again: **a bigger file is a proportionally harder scheduling problem**. Not just longer — harder. Double the object and the scheduler's own inefficiency grows by $\sqrt{2}$, before the network has done anything unusual.

That conclusion rests on an assumption the loop-scheduling literature inherited from the multiprocessor setting it was developed in: *work, once issued, is committed*. A chunk handed to a processor cannot be taken back, so the scheduler must wait out the slowest outstanding assignment, and the expected tail is of order $\tau$. That is what makes the tail term scale with chunk size, and therefore what makes chunk size a decision at all.

An HTTP range request does not have that property.

```http
GET /debian.iso HTTP/1.1
Range: bytes=1048576-8388607
```

The start offset is enforced by the server. **The far end is enforced by the client.** Nothing obliges a client to read to the end of what it asked for, and nothing has to be sent to stop. So a laggard's range can be shrunk — take its unread tail away, hand it to somebody faster — and the cost is one repair request on the taker's side and *nothing at all* on the victim's. No cancellation, no round trip, no message to the origin.

Chunk duration and reassignability, a single parameter in the classical model, come apart. Overhead is paid per **repair**, not per **chunk**. Write $V$ for the number of repair events, $n$ for the connection count and $\theta$ for the divergence deadband that triggers a repair, and the excess bound becomes

$$T - T^{\ast} \;\le\; \Big(\delta + \frac{V\delta}{n}\Big)\frac{B_{\mathrm{low}}}{\bar{B}} + \theta$$

There is no $S$ in it and no $\tau$. For a fixed repair budget the excess is independent of object size, and the chunk count has left the bound entirely.

![Excess and request count against object size, committed chunks versus range preemption](/images/hydra_regime_scaling_plot.png)

*__Figure 1__ — sweeping object size across a factor of 1024 with the adversary's rate-breakpoint count held fixed. Committed chunks track $\sqrt{T^{\ast}}$ with fitted exponents of 0.50 and 0.50 — the classical law, reproduced. Range preemption on identical rate trajectories gives 0.15 and 0.36, and stays under 0.75 s absolute across the whole sweep. Panel (b) is why: 51 → 1778 requests for the committed scheduler as the object grows, against 12 → 34.*

The residual exponent is not zero and I would not present it as zero. Equalisation is exact only instantaneously — unequal rates re-diverge afterwards — so $V$ itself grows slowly with duration even when the network is stationary. What the bound forbids, and what is absent from the measurements, is growth proportional to $\sqrt{S}$.

One honest caveat that belongs here rather than in a footnote: this does not repeal the $\Omega(\sqrt{\delta T^{\ast}})$ lower bound that binds every online uncoded algorithm in this model. An adversary that collapses the rate of whichever connection was just assigned forces a repair every time, driving $V$ up until $V\delta/n$ reaches that bound anyway. The difference is *where the cost is paid*. A committed scheduler pays it against every environment, including a completely benign one, because its chunk count was fixed in advance. A preemptive one pays it only against an adversary actively hunting it.

---

## 2. The protocol seam: which protocols can do this at all

If preemption being free is the load-bearing property, then a transport abstraction that hides whether it *is* free is actively dangerous — it lets the scheduler price reassignment decisions for HTTP and then execute them against a protocol where each one costs a round trip.

So in HYDRA the cost is part of the interface. `hydra-net`'s scheme layer sits one level above the byte-stream connector, and each protocol declares what its ranged reads can and cannot do:

```rust
pub struct Capabilities {
    /// Ranged reads at all. Without this, multi-source assembly is impossible.
    pub ranged: bool,
    /// A range can be ended by the CLIENT without telling the server.
    pub client_bounded_ranges: bool,
    /// Round trips required to stop early and be ready for the next range.
    pub preempt_cost_rtt: f64,
    /// A validator that can prove two sources serve identical bytes.
    pub has_validators: bool,
}
```

| Protocol | Ranged | Client-bounded | Preempt cost | Validator |
|---|---|---|---|---|
| HTTP/1.1 | yes | yes | 0 RTT | ETag / size + mtime |
| SFTP | yes | yes | 0 RTT | weak (`FSTAT`, `check-file`) |
| FTP | yes (`REST`) | **no** | **2 RTT** | none |

FTP is the case that motivates the whole seam. `REST <offset>` says where a transfer *starts* and there is no way to say where it ends, so shrinking a range means aborting the data connection: `ABOR` on the control channel, drain the reply, then a fresh `PASV` for the replacement data connection. Two round trips where HTTP needs zero — and that 2.0 is measured in `ftp.rs`'s own tests, not assumed.

The genuinely interesting row is SFTP, which nobody associates with download acceleration. Its read operation is `SSH_FXP_READ(handle, offset, length)`: **both** ends named by the client, on every request, pipelined over one connection with independent request ids. Shrinking a laggard's work means simply not issuing the remaining reads — no new connection, no header round trip per range. On the axis this scheduler cares about, SFTP is strictly cleaner than HTTP. It is not implemented yet; the capability entry is there because stating the property is what makes the theorem portable instead of an HTTP anecdote.

This is the part of the design I would most defend to someone who thinks it is over-abstraction. `Connector` already abstracts a byte stream — TCP, TLS, a SOCKS-relayed socket, an in-process duplex pipe. Every implementation answers "give me bytes to and from this endpoint," which is the right seam for *transport* and the wrong one for *protocol*. HTTP and FTP do not differ in how bytes move. They differ in how you ask for a range, and in what it costs to stop asking.

---

## 3. The engine: a scheduler with no clock and no sockets

`hydra-core` is `#![forbid(unsafe_code)]`, has no I/O, holds no clock, and allocates nothing in the steady state. The caller feeds it observations, calls `tick(now)`, and acts on the actions that come back. That is what lets one implementation serve both the discrete-event simulator and real HTTP: the simulated sweeps and the live transfers below exercise the same scheduler kernel, differing only in who supplies the clock and the bytes.

There are exactly three actions:

```rust
pub enum Action {
    /// Issue `GET` with `Range: bytes=lo-(hi-1)` on this connection.
    Request { conn: usize, range: Range },
    /// Stop reading this connection's current response; its range was reclaimed.
    Cancel { conn: usize },
    /// The far end of this connection's in-flight range moved DOWN to `hi`.
    /// Stop reading at `hi`.
    Shrink { conn: usize, hi: u64 },
}
```

```mermaid
graph TD
    Probe[Probe: size, ranges, validator, first RTT] --> Plan[plan::allocate: connections per mirror from ranking and ceilings]
    Plan --> Split[initial_split: maximal ranges, priority-weighted]
    Split --> Tick[tick: the scheduler kernel]

    Bytes[on_bytes: windowed rate samples] --> Tick
    Tick --> Detect[detect: dual-window ratio plus CUSUM grading]
    Detect --> Tick

    Tick --> Reclaim[stall reclaim and source backoff]
    Tick --> Repair[divergence-triggered repair]
    Tick --> Assign[work-conserving assignment]

    Repair --> Act[Request / Cancel / Shrink]
    Assign --> Act
    Reclaim --> Act
    Act --> Net[hydra-net: HTTP, FTP, TLS, SOCKS, positioned writes]
    Net --> Bytes
```

Two invariants are checked rather than hoped for, and both are property-tested: **coverage** — the held, in-flight and unassigned sets partition $[0, S)$ with no gap and no overlap, so no byte is fetched twice or missed — and **liveness** — every reachable state has an enabled transition that decreases remaining work within a bounded window.

The engine around it is deliberately boring in the places where being interesting would be a liability. Arriving ranges are written straight to their file offsets, so nothing is reassembled in RAM and peak memory sits at 2.9–4.0 MB across a 256× range of object sizes. (That design is not HYDRA's; it is DBPP's, reproduced here as evidence of a sound implementation rather than claimed as a contribution.) Per-host concurrency defaults to 4 and total to 16 — public-mirror etiquette, not physics. A publisher's Metalink ranking is honoured, but only as a prior, and only for the very first split:

> It is worth having ONLY as a prior. A ranking cannot know that the preferred mirror is currently overloaded, and this scheduler exists precisely because that is discoverable at runtime and correctable for free. […] Letting the prior persist would be strictly worse than having no ranking at all, because it would defend the wrong source against evidence.

---

## 4. The repair storm: when the mechanism ate itself

Here is the part I find most instructive, because the theory was fine and the implementation was quietly refuting it in production.

The [IEEE Networking Letters submission](https://github.com/ja7ad/hydra/tree/main/docs/letter) that formalises the bound reports it as an open defect. Against `aria2c` and `curl` over the same objects, same transport, same connection budget — 20 runs, five CRAN packages, every transfer byte-exact against a reference digest — HYDRA's median was 9.4 s against 3.6 s for `aria2c` and 3.8 s for `curl`. Sweeping the connection count on one object isolated the pathology: at $n=1$ the binary was competitive with both baselines; at $n \ge 2$ it issued 7–49 repairs on a **stationary** 5 MB transfer where the correct count is zero, and observed throughput decayed *within the run*, 439 → 306 KiB/s.

The letter's diagnosis was a mistuned divergence estimator firing on proxy-induced jitter. That diagnosis was wrong, and the way it was found wrong is the interesting bit: the simulator could not reproduce the storm, because its origin model gave every connection an independent rate and charged nothing for a request. Reality gives neither. Rebuilding the harness with the two missing terms — one shared bottleneck whose capacity is split among the connections, and a slow-start ramp after every new request — reproduced it immediately, and the cause was not statistical at all.

The scheduler shrank `conns[vi].range` and emitted nothing. The transport's fetch loop runs `while off < hi` against the `hi` it captured when the request was spawned. So the victim never learned. It kept pulling the bytes it had just been relieved of, at the same time as the taker pulled them, over the same bottleneck. Each repair cost roughly one stolen span of duplicated traffic instead of nothing — and because the duplicate traffic slowed the honest connections, it manufactured the very divergence that triggers a repair. Positive feedback, closed.

The fix is the third `Action` variant above, and a `Watermark` the transport can lower under the running fetch:

```rust
pub fn shrink_to(&self, hi: u64) {
    self.0.fetch_min(hi, Ordering::Relaxed);
}
```

`fetch_min`, not a plain store, so the direction is an invariant rather than a convention: a repair only ever gives work away, and raising a bound would hand a connection bytes another connection may already hold — a coverage violation, not an optimisation.

![Oracle ratio and duplicated bytes with and without shrink propagation](/images/hydra_repair_storm_plot.png)

*__Figure 2__ — the shared-bottleneck harness, 5.3 MB over one 1.4 MB/s link, $\delta = 0.12$ s, 12 seeds, run against the current scheduler. With the shrink unpropagated the transfer pays a flat ~35% over the fluid oracle for doing nothing but re-labelling bytes that were already arriving, and 0.45–0.85 MB of the object crosses the wire twice. With the fetch loop honouring it, the ratio is 1.04–1.06 and duplicate traffic is exactly zero. The theory was never implicated: the same policy, in a harness without the zombie term, never showed it.*

Two things I want to be precise about. First, this is the harness, not the wild: the live re-measurement against `aria2c` has not been redone since the fix, so figure 5(a) still shows the binary losing. Second, a scheduler that simply never repaired would score perfectly on a stationary transfer, which makes it the right test for a storm and the wrong test for the mechanism. So the same harness carries a collapse arm — one connection's share drops to 5% at 30% progress — and there, repairs still fire (2.0, 3.9 and 4.8 of them at $n = 2, 4, 8$) and the ratio stays at 1.04.

---

## 5. A divergence is not an opportunity

The storm was a transport bug, but it exposed a scheduling assumption that was just as wrong, and fixing that one produced the ideas I think are the most transferable in the whole engine.

The equalisation the scheduler solves — give the taker $x$ bytes such that

$$\frac{\text{left} - x}{r_v} \;=\; t_{\mathrm{eta}} + \delta + \frac{x}{r_t}$$

— treats $r_t$ as *capacity that $x$ bytes can be moved onto*. True when the connections have independent bottlenecks: separate mirrors, separate paths. False in the case that dominates real use, which is several connections to one origin sharing one bottleneck. There the taker's rate is not spare capacity, it is a share of the same capacity the victim is already using. Moving bytes across does not make them arrive sooner. It re-labels which connection carries them, and charges a setup for the privilege.

Worse, the per-connection divergence that triggered the repair is largely a property of the *path*, not of the assignment. Flows sharing a bottleneck settle at persistently unequal shares — roughly $1/\mathrm{RTT}$, with congestion-window history making the asymmetry outlive any single round trip. A repair cannot move that. So the divergence survives the repair, and re-triggers it.

Three guards follow, and none of them is a tuning constant.

**The deadband is floored at the setup cost.** $\theta = \sqrt{\delta T_{\mathrm{rem}}/n}$ is the right *shape* — it is the granularity trade-off — but it is unbounded below, and it approaches zero from two directions that both make repair a worse idea rather than a better one: $T_{\mathrm{rem}}$ shrinks as the transfer finishes, and $n$ grows with concurrency. The deadband is therefore narrowest exactly when a repair has the least remaining time to earn its cost back and the most competitors to pay it against. Measured on the shared-bottleneck harness, $\theta$ reached 0.061–0.081 s against a $\delta$ of 0.12 s. Every repair triggered in that regime spent a full setup to recover a divergence smaller than the setup. So $\delta$ is the floor — not a constant somebody picked, but the break-even point, measured per source, so a high-RTT path widens it automatically.

**The repair must pay for itself, against the makespan.** Compare the projected makespan now against the makespan after, where "after" charges the setup and credits only the improvement in the *worst* finishing time — because a makespan is a max, not a sum, and improving anything other than the laggard buys nothing. Require the gain to exceed $\delta$, not merely to be positive. And when a rate is unmeasured, the comparison is `NaN`, which must *refuse* the repair rather than fall through either way: the setup cost is certain and the gain is not.

![Deadband floor and the profitability test](/images/hydra_deadband_plot.png)

*__Figure 3__ — left: the unfloored deadband dives below the price of a repair in the endgame and at high concurrency, which is exactly where it must not. Right: tracing the accept rule over one exchange — unless a large share of the taker's observed rate is genuinely spare, the repair is a loss even though the divergence that triggered it is real.*

**Both sides of the exchange must be warm, and neither may still be climbing.** This one was found by tracing, and it is the failure I would least have predicted. On a uniform four-connection transfer with a 100 ms round trip, 1109 repair decisions were evaluated, almost all inside the first 0.6 s. The ones that executed had a victim reporting 0.2 MB/s and a projected 1092 s to finish, against a taker whose first sample happened to be larger. Both numbers were slow-start artefacts a second away from 70 MB/s. The "gain" was hundreds of seconds that did not exist, and the repair moved half a range that would have arrived on its own.

Guarding only the victim made it *worse* — repairs went from 2–10 to 9–15 — because the taker was equally unmeasured and the choice merely shifted. Both sides have to be warm. And warm-up counted in samples is not enough either, because slow start lasts a number of round trips while the sample window is fixed: on a 100 ms path the first repair fired at $t = 1.4$ s on a 3:1 rate ratio that was 1:1 by $t = 2.0$ s, after moving 256 MB. Hence `rising()` — if the short and long averages still disagree by more than 15%, the connection is not slow, it is early, and "is this connection slow?" has no answer yet.

There is one exception to all of it: a victim the detector has already graded as collapsing. That grade is itself a measurement, and pre-empting a collapse is the entire point of having a detector.

---

## 6. What preemption actually costs

"Free" deserves an asterisk, and the engine is the place where it gets one.

Every request used to carry `Connection: close`. For a client that fetches one object per connection, a fresh TCP handshake and a fresh TLS handshake is a fixed cost you notice once. For this one it is *the* central cost, because the whole premise is $n$ concurrent ranges against the same origin — so a transfer opened $n$ connections to a host it was already talking to, and every repair opened another.

It also corrupts the scheduler's own arithmetic, in a way that took me a while to see. The deadband and the profitability test are both denominated in $\delta$, the measured per-request setup cost. With no reuse, $\delta$ *is* a full handshake — hundreds of milliseconds through a proxy or to a distant TLS origin — so the scheduler prices every decision against a number that connection reuse would cut by an order of magnitude. Reuse does not merely make requests cheaper. It makes the cost model describe something closer to the protocol's actual behaviour.

But a pooled connection is only safe when the client knows *exactly* where the previous response ended, because anything left unread becomes the first bytes of the next response — a silent corruption in which the next range's body is prefixed with the previous one's tail, lands at the wrong offsets, and still produces a file of the right length. So a socket goes back into the pool only if the body was read to a known length, the server did not answer `Connection: close`, the response was HTTP/1.1, and — the condition specific to this scheduler —

> the range was not shrunk mid-flight.

When a repair lowers a connection's far end, the fetch loop stops early *by design*. The server is still sending toward the original end, so the socket has an unknown amount of unread body in it. That connection is exactly the unsafe case and must be dropped rather than pooled.

Which gives the honest form of the claim. Preemption is free on the wire: no cancellation, no round trip, nothing told to the server. What it costs is the reusability of that one connection — and since reuse is what makes $\delta$ small, every repair very slightly raises the price of the next one. That is not in the bound. It should be.

---

## 7. Grading the connection, not smoothing its rate

Which brings us to the estimator. The scheduler cannot repair a divergence it has not observed, and the original EWMA-only estimator had a *fixed* detection cost of roughly 0.25–0.9 s when a source's rate collapsed by ~97%. That cost showed up as a makespan ratio of 2.44× on a 12 MB object falling to 1.02× at 192 MB — the signature of a constant that amortises, not a per-byte inefficiency.

The lag is structural, not a tuning failure. An EWMA is a *smoother*; asking it to detect a step change is asking the wrong question of it, and after a collapse the very samples it needs arrive at the collapsed rate.

So HYDRA grades instead, on two independent mechanisms that fail in different directions. A **dual-window ratio** — a short EWMA against the connection's established rate — responds within one window but is noisy. A **two-sided CUSUM** accumulates normalised downward deviations with slack $k = 0.25$ and fires at $h = 2.0$; slower on a hard collapse, far more resistant to variance, and it catches slow degradation the ratio test smooths over. The scheduler acts on the grade — `Healthy`, `Suspect`, `Degraded`, `Stalled`, `Dead` — not on a raw number, and repair may fire on `Suspect` long before the stall timeout would expire.

The subtle part is one line:

```rust
if self.cusum_down <= 0.0 {
    self.long = 0.08 * r + 0.92 * self.long;
}
```

The reference level is **frozen** while evidence is accumulating. Textbook CUSUM, and getting it wrong is invisible: an adaptive reference chases the drop, the normalised deviation shrinks toward the slack, and evidence never accumulates. Measured on a sustained 45% decline, an adaptive reference plateaus at 0.93 against a threshold of 2.0 — it never fires, because the decline has quietly become normal.

![Detector grades on a hard collapse and a slow fade](/images/hydra_collapse_detector_plot.png)

*__Figure 4__ — the shipped `CollapseDetector` driven over two synthetic traces. A 97% collapse is graded `Suspect` one 0.2 s window after onset and `Degraded` two windows after. A fade to 45% over three seconds — which no single sample registers as a step — is graded 4.8 s in, by the CUSUM, while the frozen reference holds the line that makes the accumulation possible. The plain EWMA is drawn for contrast: it is a perfectly good estimator and a poor detector.*

Two details that are load-bearing and look like trivia. Rate is sampled over a fixed 200 ms window rather than per arrival, because an arrival is one `read()` return and a read served from already-buffered socket data completes in microseconds — implying 128 MiB/s on a link doing well under 1 MiB/s. Those inflated samples raise the detector's reference, after which every honest sample looks like a collapse against it, and all eight connections of a healthy transfer grade as bad. And silence is fed to the detector from the wall clock, because a connection delivering nothing produces no rate samples at all; halfway to the stall timeout with nothing arriving is already evidence.

The false-positive cost here is real and asymmetric: a missed collapse costs up to a stall timeout, while a spurious detection costs one $\delta$ per unnecessary repair, and on a high-RTT path $\delta$ is hundreds of milliseconds. The test asserting that a *stable noisy* connection is never flagged is as load-bearing as the detection tests.

---

## 8. The second knob: measuring concurrency instead of asking about it

The other number the user is normally asked for is connection count, and the standard way to compute it is to probe: fetch a slab with one connection, then two, then four, compare aggregate goodput, settle where the marginal gain stops paying.

HARP (Kim, Yildirim & Kosar, SC'16) names the objection directly — probing captures instantaneous load but "may bring too much probing overhead," because each sample is an extra transfer paid for before the real one begins. Measured here on a 3.15 MB object over a live path, the climbing probe made the whole transfer **1.96× slower than not probing at all**: 18.2 s against 8.3 s, paired across 9 interleaved repetitions, $p = 0.004$. The search cost more than the concurrency it found could recover. HARP's answer is to amortise the samples across a historical corpus of past transfers.

A downloader has a cheaper route. The probe is only necessary because concurrency is fixed when the transfer starts — so make it adjustable mid-transfer and run the same search *on the object itself*. Start at one connection, measure aggregate goodput over a short window, admit another while the marginal gain justifies its setup cost, stop. Every byte moved during the search is a byte of the object that had to be fetched anyway. The search is free in bytes; its only cost is arriving at the final concurrency a few windows late.

![Live binary comparison and where the concurrency search gets its samples](/images/hydra_concurrency_plot.png)

*__Figure 5__ — panel (a) is HYDRA losing, and it is in the letter for the same reason it is here: fixed multi-connection configurations are slower than one stream on four of five live CRAN objects. Part of that is a correct prediction — the path is saturated at $n^{\ast} = 1$–$2$, so opening four connections only multiplies $\delta$ — and part of it was the repair storm of section 4, which had not been diagnosed when these runs were made.*

Three things make the in-band version harder than it sounds, and all three were learned by getting them wrong:

The bar cannot be a fixed fraction of the single-connection rate, because that asks "did throughput improve at all," and on a warming path the answer is always yes — flows admitted a window ago are still opening their congestion windows. The search reached the ceiling in 9 of 12 runs on paths where a single connection was 1.8–3.2× faster than the ceiling it chose. The right question is "did throughput improve *as much as adding these connections should have*," so the observed ratio is compared against the ratio of connection counts. That separates a genuinely parallel path from a saturated one without knowing the link's capacity or RTT.

The window length cannot be scaled by $\delta$ alone, in either direction. Too long (tied to $\delta$ with no ceiling) and reaching 8 connections took 16–32 s on a path where $\delta$ was 0.5–1.0 s — longer than the entire 3.15 MB transfer, and 2.78× slower than a fixed `-x 8`. Too short and every level looks like it is still improving, so the search runs to the ceiling on a path one stream already saturates. No single value satisfies both, which is why the ramp no longer climbs from one on optimism; it starts low and only *adds* on direct evidence of headroom, because the asymmetry is measured: starting at one connection is statistically indistinguishable from a fixed baseline, while a fixed `-x 8` cost 1.37–3.04×, and on a saturated path 3.6×.

And the settle delay cannot be a duration alone. The windows are scaled by the per-*request* setup cost, 50–100 ms on a pooled connection. Admitting a *connection* costs a TCP handshake, a TLS handshake and a first byte — 1.2–1.6 s on a 250 ms-RTT path. So the window opened and closed while the new connection was still handshaking, the level measured as no better than the one below it, and the search settled at **one** on a path with real headroom. It was reported from the field as "only two of eight connections start." A low-RTT path escapes by luck, which is what made it look path-specific rather than systematic. The transport now reports how many connections are actually delivering, and the window does not open until the level is genuinely on the wire.

There is one more piece, and it is not an optimisation at all. A search that settles below the budget leaves most connection rows idle, and a row that says only "waiting" looks like a dropped connection rather than a decision — which is exactly how a transfer behaving *correctly* came to be filed as a bug. So the scheduler carries a `LimitReason` it never acts on:

```rust
pub enum LimitReason {
    None,
    Measuring,
    Measured { chosen: usize, chosen_rate: f64, tried: usize, tried_rate: f64 },
    Refused { serving: usize },
    Starved { serving: usize },
}
```

An adaptive system that cannot explain its own decisions will have those decisions reported as defects. Carrying the evidence costs one enum.

---

## 9. What this buys, and where it does not help

The practical consequence is subtractive, which is the part I like. Two user-set knobs become computed quantities. Request duplication — the standard hedge against the straggler tail — becomes unnecessary, because repair already reassigns the laggard's tail; over 480 hedged runs against 480 matched controls the mean makespan gain was exactly 0.000 in every configuration, at 8.5% mean duplicate bytes, so the mechanism is not shipped. And the failure mode that actually matters turns out to be a consequence of commitment rather than of inadequate tuning: under mirror failure, both static-split policies never finish at all — they deliver 86.2% and 87.4% of the object and then stall permanently, because bytes committed to a dead source are never reassigned. A downloader that stalls at 87% is not 2.9× worse than optimal. It is unusable, and no choice of connection count repairs it.

Where it does not help, in order of how much it costs:

On a path already saturated by one connection, no scheduler can recover parallelism, and this one correctly declines to try. That is figure 5(a), and it is a prediction of the fluid-oracle bound rather than a defeat — but it does mean the honest answer to "will HYDRA make my download faster?" is often *no, and it will tell you why*.

$\delta$ captures handshake latency but not the throughput ramp. A repair that has to dial restarts in slow start, so on a high bandwidth-delay-product path it costs more than the bound charges. Pooling is what keeps that rare — including for the size probe, whose connection used to be thrown away, costing 1.6–2.0 s of setup on transfers whose bodies took 3.7–5.5 s — but section 6 is the reason it cannot cover every case.

The result is stated for HTTP/1.1 over separate connections, which is the pessimistic case. In HTTP/2 and HTTP/3 a new range is a stream on an established connection and a reset is nearly free, so $\delta$ falls toward zero and the bound degenerates to $\theta$ — at the cost of stream flow control and, for HTTP/2, head-of-line blocking the model omits.

Preemption is free *on the wire* but not at the origin, which may have issued a read-ahead for bytes the client stops reading. The cost is origin I/O and log noise rather than client makespan, and a tighter deadband against a stricter origin could trip anti-abuse mechanisms.

And the live comparison against `aria2c` still stands at 2.7× in `aria2c`'s favour, measured before the storm was diagnosed. Until that is re-run as a binary-level comparison, the scaling result is established in simulation and under a controlled HTTP transport, and nowhere else.

---

## 10. The recipe

If you are building something in this shape, the transferable part is short:

Give every connection a **maximal** range rather than a computed chunk, and let the unassigned set be the single source of truth for what remains. Trigger repair on **divergence of projected finish times**, not on idleness — with maximal ranges no connection goes idle until the transfer is nearly over, so an idleness-triggered work stealer learns of a straggler far too late to help. This is the one design detail that could not be derived analytically; it came out of the simulator falsifying an earlier chunked design.

Make every repair **prove it pays**: floor the deadband at the measured setup cost, compare makespans as maxima rather than sums, require the gain to exceed one $\delta$, and refuse on any unmeasured quantity. Require both sides of an exchange to be warm and settled, because a rate that is still climbing is a measurement of slow start, not of capacity.

**Grade** connections rather than smoothing them, freeze the reference while evidence accumulates, and sample rate over wall-clock windows rather than per arrival.

**Probe for the saturation point in-band** instead of exposing a connection-count setting, and record why the search stopped where it did.

Skip request duplication; it buys nothing once repair is in place.

And propagate the shrink to whatever is actually reading the socket. That one is worth 35% of your makespan and every duplicate byte on the wire.

---

### Code, data and the letter

Everything above is open: the [engine](https://github.com/ja7ad/hydra), the [feature architecture](https://hydra.javad.dev/features.html), the measurement harness, the raw result files, and the letter that formalises the bound and reports the same negative results. The storm reproduction is `cargo run --release -p hya-core --example storm`, which is where figure 2 comes from — 12 seeds, both arms, on your machine in under a second.

