Conversation
A fixed concurrency limits throughput to about N x chunk size / round trip time, so with the default of 10 a fast connection with some latency stays mostly idle. With '-n -1', or "n": -1 in the store options of the config, requests to remote stores go through an AdaptiveStore that adjusts the number of concurrent requests while the command runs, up to 128. Work bound by the CPU, chunking in 'make' and hashing in 'verify' and 'verify-index', uses the number of CPUs instead. The limit is sized like TCP BBR sizes its congestion window. It starts at 10 and doubles after each window of requests until throughput stops growing, then stays at twice the highest throughput times the lowest latency. A failed request halves it. Latency alone isn't a reliable signal: responses sharing a link take longer well before it's full. Stores size their connection pools for the maximum when the concurrency is adaptive.
A store configured with an adaptive concurrency raises the number of workers a command runs to 128. Only the adaptive store was limited, so the other remote stores in the same command, a cache, further stores or failover group members, got up to 128 concurrent requests instead of their own concurrency, with most of their connections reopened on every request. Commands now work out the number of workers before opening the stores. When it's above the concurrency given on the command line, remote stores with a fixed concurrency are wrapped in a limiter holding them to it. Without an adaptive store nothing changes. AdaptiveStore becomes LimitedStore, which takes either the adaptive or a fixed limiter.
RyuzakiKK
approved these changes
Sep 21, 2026
RyuzakiKK
left a comment
Contributor
There was a problem hiding this comment.
I did some manual tests of these changes and the adaptive concurrency adjustments seem to be working as expected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #430.
A fixed concurrency limits throughput to about N × chunk size / round-trip time. With the default of 10 and 64KiB chunks, a connection with a 50ms round trip tops out around 100 Mbit/s however fast it is.
Behavior
-n -1on the command line, or"n": -1in a store's options in the config, lets desync choose:LimitedStorewith an adaptive limiter (NewAdaptiveStore), which adjusts the number of concurrent requests to each store while the command runs, up to 128. Commands run 128 workers when any of their stores is adaptive, and the limit decides how many of them have a request in flight. Local stores aren't limited.NewLimitedStore). Without an adaptive store, nothing is wrapped.makeand hashing inverifyandverify-index, usesruntime.GOMAXPROCS(0).A plain number behaves as before.
0and values below-1are still rejected on the command line. In the config,0is the same as not settingn, which is why-1rather than0is the value for this.Store connection pools (HTTP and OCI idle connections, S3 transport, SFTP and SSH sessions) are sized for the maximum when the concurrency is adaptive. OCI prune, which doesn't adapt, runs 10 removals at a time with
-1. With--verbose, every change of a limit is logged.How the limit is picked
The limiter works like TCP BBR sizes its congestion window. It starts at 10 and doubles after each window of requests (at least 20, about one round trip) until throughput grows by less than 25% for three windows in a row. From then on the limit is twice the highest throughput of the last 10 windows times the lowest average latency seen, which is twice the number of requests the network and the store can process at once. The headroom lets throughput grow when the network or the store get faster, and the limit follows. A failed request, other than a missing chunk, halves the limit and discards the throughput history. Windows that don't reach the limit don't change it.
Latency alone isn't a reliable signal here. Responses sharing a link take longer well before the link is full, so a latency-based limit (TCP Vegas style) backs off early: about half the achievable throughput at a 20ms round trip.
Benchmark
extractof 256MB of random data (64KiB average chunks) fromchunk-server, over loopback shaped withtc netemin a network namespace. Throughput in Mbit/s:-n 10-n 100-n -1At a 100ms round trip, filling the link would take about 190 requests in flight, so both are bounded by the ceiling.
The limit settles higher than the link needs, since the lowest latency is first measured while connections are still being set up. That doesn't cost throughput, but keeps more connections open than necessary.