What happens
Hosted release downloads (GET /dl/:slug/releases/:releaseId/:file, and the channel latest route) are streamed through the Worker from R2 (serveHostedAsset → APK_BUCKET.get). The Worker runs with "placement": { "mode": "smart" } (worker/wrangler.hands.jsonc), which places it near D1, in San Jose, so every byte of a download flows R2 → SJC Worker → client. Nothing caches it at the edge: the response has cache-control: public, max-age=31536000, immutable but no cf-cache-status, since a Worker's response doesn't populate the CDN cache.
$ curl -sI https://hands.build/dl/ferry-cli/releases/633a9164-619f-43e8-8732-901a59f75efb/darwin-arm64.gz
cache-control: public, max-age=31536000, immutable
cf-placement: remote-SJC
Impact
From Asia (Beijing, 2026-09-30), the same 43 MB ferry-cli download ran at very different speeds depending on the route, all through the SJC Worker:
| When |
Direct |
Via a local proxy |
| 16:50 CST |
~96 KB/s |
~730 KB/s |
| 17:40 CST |
~3.2 MB/s |
~170 KB/s |
A remote Ferry update of a Mac spent ~6 minutes on this download, and the next one ~4 minutes. ferry-installer did nothing wrong: it saw only a slow stream.
Suggestions
Any of these would serve the bytes from near the client:
- Put hosted-asset responses in the edge cache with the Cache API (
caches.default), keyed on the immutable release URL. The bytes are immutable per release id, so there's nothing to invalidate.
- Redirect (302) to a cacheable R2 public or custom-domain URL, or a presigned GET, after the D1 lookup. The redirect stays tiny, even from SJC.
- Keep smart placement for API routes but not
/dl/*, for example by splitting the download route into its own Worker without placement.
Reported from botiverse/ferry, whose installer and remote updates download through these routes.
What happens
Hosted release downloads (
GET /dl/:slug/releases/:releaseId/:file, and the channellatestroute) are streamed through the Worker from R2 (serveHostedAsset→APK_BUCKET.get). The Worker runs with"placement": { "mode": "smart" }(worker/wrangler.hands.jsonc), which places it near D1, in San Jose, so every byte of a download flows R2 → SJC Worker → client. Nothing caches it at the edge: the response hascache-control: public, max-age=31536000, immutablebut nocf-cache-status, since a Worker's response doesn't populate the CDN cache.Impact
From Asia (Beijing, 2026-09-30), the same 43 MB
ferry-clidownload ran at very different speeds depending on the route, all through the SJC Worker:A remote Ferry update of a Mac spent ~6 minutes on this download, and the next one ~4 minutes.
ferry-installerdid nothing wrong: it saw only a slow stream.Suggestions
Any of these would serve the bytes from near the client:
caches.default), keyed on the immutable release URL. The bytes are immutable per release id, so there's nothing to invalidate./dl/*, for example by splitting the download route into its own Worker without placement.Reported from botiverse/ferry, whose installer and remote updates download through these routes.