Summary
This is the SWT-Bench-specific sub-issue for the broader image build regression tracked in #504.
After going through the historical SWT runs carefully, the picture is:
- The best historical cold SWT run we still have is much faster than current March runs.
- SWT was already slower in early March before the shared-cache SDK change landed.
- The latest and worst SWT collapse happens when the SDK starts exporting a shared registry cache from every parallel SWT build.
- SWT is especially sensitive because it fans out across hundreds of mostly unique base images, so the shared cache is cold at the start and many workers try to write to the same cache ref concurrently.
Cold vs warm classification
These runs should not be used to measure cold-build throughput:
23000734281: fully warm, artifact manifest shows 433 skips / 0 builds.
22989698608: fully warm, artifact manifest shows 433 skips / 0 builds.
22985173172: fully warm, artifact manifest shows 433 skips / 0 builds.
22967516108: mixed, artifact manifest shows 288 skips / 145 builds.
These are the most useful comparison points:
21870877504 (2026-02-10, SDK cfe52af, best historical cold baseline available)
- no
already exists. Skipping build. lines in the run log
- command used
--max-workers 4
23001334237 (2026-03-12, SDK aa9df69, near-cold partial run)
- manifest mostly real builds before cancellation
- command used
--max-workers 4
23008709458 (2026-03-12, SDK d0c1a39, near-cold partial run)
- manifest shows only
12 skips in the first 170 completions
- command used
--max-workers 4
23017371689 (2026-03-12, SDK fc962c4, latest bad run)
- manifest shows
44 builds / 27 skips before cancellation
- command used
--max-workers 16 --build-batch-size 50
Throughput regression
Useful milestone timings from the raw GitHub logs:
| Run |
SDK |
Classification |
50/433 |
100/433 |
150/433 |
21870877504 |
cfe52af |
best cold baseline |
18m29s |
40m52s |
55m41s |
23001334237 |
aa9df69 |
near-cold |
41m07s |
1h20m24s |
- |
23008709458 |
d0c1a39 |
near-cold / lightly mixed |
48m18s |
1h41m39s |
2h30m46s |
23017371689 |
fc962c4 |
mixed, latest bad run |
2h00m52s |
- |
- |
Equivalent completion throughput:
21870877504: about 162 images/hour to 50, 147 images/hour to 100
23001334237: about 73 images/hour to 50, 75 images/hour to 100
23008709458: about 62 images/hour to 50, 59 images/hour to 100
23017371689: about 25 images/hour to 50
So there are really two regressions:
- Feb cold baseline -> early March pre-shared-cache runs: roughly
2.0x to 2.6x slower.
- Early March pre-shared-cache runs ->
fc962c4 shared-cache run: another roughly 2.4x to 3.0x slower by the 50-image mark, even though the run was configured with higher nominal concurrency.
Relative to the best cold baseline, the latest fc962c4 run is about 4.9x slower at the 50-image mark.
What changed in code
Between the good February baseline and early March
The early March runs (aa9df69 / d0c1a39) are still using the older SWT workflow shape with --max-workers 4.
Those runs do include SDK build-path changes relative to cfe52af, including a heavier builder environment and then the prebuilt-sdist optimization. The prebuilt-sdist optimization helps remove repeated uv build --sdist, but it does not explain the remaining SWT slowdown by itself.
Between d0c1a39 and fc962c4
There are two important changes in the latest bad regime:
- The SWT workflow was changed from default
max-workers=4 to max-workers=16, and agent image submit batch size was raised to 50.
- The SDK build path changed from exporting one registry cache to exporting two caches for pushed builds:
- the image-specific cache ref
- a shared cache ref
In openhands/agent_server/docker/build.py, fc962c4 introduces shared_cache_tag / shared_cache_tags and then emits two --cache-to type=registry,... destinations for a pushed build.
That change is the key new behavior for the worst SWT run.
Why SWT is hit harder than SWE
SWT expands to a very large set of distinct base images. On the current dataset, collect_unique_base_images() yields 433 unique base images.
That means the first wave of a cold SWT run gets very little value from a shared cache that has not been seeded yet. At the same time, with the fc962c4 behavior, many workers are all trying to export to the same shared registry cache ref.
So SWT pays both sides of the penalty:
- little cache reuse at the start, because the build graph is cold and the images are diverse
- heavy contention and upload cost on
cache-to because many workers write the same shared ref
Direct evidence from the fc962c4 run
The run-level progress already looks bad, but the per-image logs make the root cause much clearer.
Representative examples from 23017371689:
astropy-12907
- image export itself took about
167s
- the two registry cache exports then took about
870s and 874s
django-10554
- image export itself took about
79s
- the two registry cache exports then took about
2294s and 2301s
In other words, some individual SWT builds are spending 15 to 38 minutes after the image is already built, almost entirely in exporting cache to registry.
That strongly points at docker buildx --cache-to type=registry,... as the current dominant bottleneck.
Root-cause hypothesis
Current best explanation:
- There was already an earlier SWT slowdown between
cfe52af and the early March SDKs (aa9df69 / d0c1a39). That older slowdown is likely due to SDK build-path changes that made the build heavier even before shared cache was introduced.
- The latest severe SWT collapse is caused by the shared registry cache export added in
fc962c4.
- The workflow change to
--max-workers 16 and --build-batch-size 50 likely amplifies the problem, because more cold SWT builds are now trying to export the same shared cache concurrently.
- The old repeated-sdist issue is no longer the primary SWT bottleneck in these latest runs, because the slow shared-cache runs are already reusing a prebuilt sdist.
Implications
For SWT specifically, the main problem now is not the Dockerfile's main build steps. It is the cache export strategy under cold, high-fanout image building.
If we want to recover SWT throughput, we likely need to change the cache strategy, not just keep optimizing sdist creation.
Likely next steps
- Disable shared
cache-to export for the normal parallel SWT worker pool.
- If we still want a shared cache, seed it once in a controlled warm-up step instead of having every worker write it.
- Re-test
fc962c4 behavior at --max-workers 4 to separate pure shared-cache cost from the concurrency amplification.
- Separately isolate the earlier Feb -> early March slowdown, which predates
shared_cache_tag.
References
Summary
This is the SWT-Bench-specific sub-issue for the broader image build regression tracked in #504.
After going through the historical SWT runs carefully, the picture is:
Cold vs warm classification
These runs should not be used to measure cold-build throughput:
23000734281: fully warm, artifact manifest shows433skips /0builds.22989698608: fully warm, artifact manifest shows433skips /0builds.22985173172: fully warm, artifact manifest shows433skips /0builds.22967516108: mixed, artifact manifest shows288skips /145builds.These are the most useful comparison points:
21870877504(2026-02-10, SDKcfe52af, best historical cold baseline available)already exists. Skipping build.lines in the run log--max-workers 423001334237(2026-03-12, SDKaa9df69, near-cold partial run)--max-workers 423008709458(2026-03-12, SDKd0c1a39, near-cold partial run)12skips in the first170completions--max-workers 423017371689(2026-03-12, SDKfc962c4, latest bad run)44builds /27skips before cancellation--max-workers 16 --build-batch-size 50Throughput regression
Useful milestone timings from the raw GitHub logs:
21870877504cfe52af18m29s40m52s55m41s23001334237aa9df6941m07s1h20m24s23008709458d0c1a3948m18s1h41m39s2h30m46s23017371689fc962c42h00m52sEquivalent completion throughput:
21870877504: about162images/hour to 50,147images/hour to 10023001334237: about73images/hour to 50,75images/hour to 10023008709458: about62images/hour to 50,59images/hour to 10023017371689: about25images/hour to 50So there are really two regressions:
2.0xto2.6xslower.fc962c4shared-cache run: another roughly2.4xto3.0xslower by the 50-image mark, even though the run was configured with higher nominal concurrency.Relative to the best cold baseline, the latest
fc962c4run is about4.9xslower at the 50-image mark.What changed in code
Between the good February baseline and early March
The early March runs (
aa9df69/d0c1a39) are still using the older SWT workflow shape with--max-workers 4.Those runs do include SDK build-path changes relative to
cfe52af, including a heavier builder environment and then the prebuilt-sdist optimization. The prebuilt-sdist optimization helps remove repeateduv build --sdist, but it does not explain the remaining SWT slowdown by itself.Between
d0c1a39andfc962c4There are two important changes in the latest bad regime:
max-workers=4tomax-workers=16, and agent image submit batch size was raised to50.In
openhands/agent_server/docker/build.py,fc962c4introducesshared_cache_tag/shared_cache_tagsand then emits two--cache-to type=registry,...destinations for a pushed build.That change is the key new behavior for the worst SWT run.
Why SWT is hit harder than SWE
SWT expands to a very large set of distinct base images. On the current dataset,
collect_unique_base_images()yields433unique base images.That means the first wave of a cold SWT run gets very little value from a shared cache that has not been seeded yet. At the same time, with the
fc962c4behavior, many workers are all trying to export to the same shared registry cache ref.So SWT pays both sides of the penalty:
cache-tobecause many workers write the same shared refDirect evidence from the
fc962c4runThe run-level progress already looks bad, but the per-image logs make the root cause much clearer.
Representative examples from
23017371689:astropy-12907167s870sand874sdjango-1055479s2294sand2301sIn other words, some individual SWT builds are spending 15 to 38 minutes after the image is already built, almost entirely in
exporting cache to registry.That strongly points at
docker buildx --cache-to type=registry,...as the current dominant bottleneck.Root-cause hypothesis
Current best explanation:
cfe52afand the early March SDKs (aa9df69/d0c1a39). That older slowdown is likely due to SDK build-path changes that made the build heavier even before shared cache was introduced.fc962c4.--max-workers 16and--build-batch-size 50likely amplifies the problem, because more cold SWT builds are now trying to export the same shared cache concurrently.Implications
For SWT specifically, the main problem now is not the Dockerfile's main build steps. It is the cache export strategy under cold, high-fanout image building.
If we want to recover SWT throughput, we likely need to change the cache strategy, not just keep optimizing sdist creation.
Likely next steps
cache-toexport for the normal parallel SWT worker pool.fc962c4behavior at--max-workers 4to separate pure shared-cache cost from the concurrency amplification.shared_cache_tag.References
aa9df69): https://github.com/OpenHands/benchmarks/actions/runs/23001334237d0c1a39): https://github.com/OpenHands/benchmarks/actions/runs/23008709458fc962c4): https://github.com/OpenHands/benchmarks/actions/runs/23017371689