hachiflow.com
エクスポート

200 MB の壁。100 GB のワークスペースを外に出すまで

CDN やプロキシを通るリクエストにはボディサイズの上限があり、顧客エクスポートは一つのアーカイブです。マルチパートアップロードは地味で正しい解決策ですが、260 MB のアーカイブが 1 バイトも欠けずに反対側へ出てくるまで、完成とは呼びませんでした。

The promise and the ceiling

We sell workspaces with 100 GB of file storage, and we promise that a customer can leave with everything. A customer export is one archive. And every HTTP request that travels through CDNs and proxies has a per-request body ceiling; on our path it was 200 MB. Put those three sentences together and the conclusion is ugly: a workspace that had really used its storage could not leave.

A data-portability promise you cannot execute is worse than no promise, because the customer discovers it at the moment of leaving, the worst possible moment to learn anything. So the export path had to stop depending on the archive fitting inside one request.

The fix is boring, which is the compliment

Multipart upload: split the archive into uniform parts, upload them one after another, each comfortably under the ceiling, and let the storage layer reassemble them into a single object. The store confirms the completed whole by echoing back the size it assembled, and the client checks that echo against the bytes it sent. No cleverness, no new protocol. The ceiling stops being a property of the archive and becomes a property of a part, and parts are whatever size we say they are.

Small archives still go up in one request; the multipart machinery engages past a threshold. What matters is that the decision is made on the archive's actual size, so no workspace is ever routed into the wall again.

一つのアーカイブ、260 MB リクエストあたり 200 MB 一括リクエスト: 拒否 均一なパートを順番にアップロード ストレージ内で再結合 etag …-5 ストアのフィンガープリントがパート数を数える
一つのリクエストでは、リクエストあたりの上限を越えてアーカイブを運べません。均一なパートなら運べます。ストレージ層自身のフィンガープリント、パート数を数える etag のサフィックスが、再結合が本当に起きた領収書です。失敗したときは必ずクライアントが中止します。アップロード途中のパートは、目に見えないまま課金されるストレージだからです。

The failure mode nothing lists

Multipart upload has one sharp edge. Until the final assembly step, uploaded parts are invisible: no listing shows them, no object exists yet, and the storage bills for them anyway. A client that crashes halfway and walks away leaves paid-for, unreachable bytes that nothing will ever surface.

So the rule is absolute: on any failure, the client must explicitly abort the upload, which tells the store to discard the accumulated parts. Our client aborts on every failure path, deliberately, because storage you cannot see is the kind of leak that survives every audit that only looks at what exists.

A green test suite was not the proof

The multipart client passed its tests against a strict fake of the storage API. Then we did the thing tests cannot do: loaded our own workspace past the old ceiling and pulled a real export, end to end, against production.

The archive came out at just over 260 MB, built in under a minute, downloaded whole, byte for byte. And the storage layer volunteered the receipt: its fingerprint for the object carried the multipart suffix, an etag ending in -5, the store's own way of saying it assembled the object from five parts. That suffix cannot appear on a single-request upload. It is the one line in the drill log that proves the new path ran, rather than the old path getting lucky with a smaller archive.

One extracted file was re-hashed and matched its content address, the export's own integrity story doing its job. The export remains what it always was: a verifiable record of your workspace, not a restore button.

Test the ceiling you sell

The lesson generalizes. Our test suite exercised the ceiling we had, with fakes; the pricing page sells a much higher one. The only honest evidence is a real workspace pushed past the point where the old path used to break, watched end to end. A 260 MB drill is not a 100 GB drill, and we will keep raising the bar, but the wall itself is gone: from here, size adds parts, not new mechanisms. Test the ceiling you sell, not the ceiling you have.

← フィールドノート一覧