Skip to content

perf(upload): WebCrypto chunk encryption + resilient part retry - #1

Merged
chogarcia merged 2 commits into
mainfrom
perf/webcrypto-aes-and-part-retry
Jul 2, 2026
Merged

perf(upload): WebCrypto chunk encryption + resilient part retry#1
chogarcia merged 2 commits into
mainfrom
perf/webcrypto-aes-and-part-retry

Conversation

@chogarcia

Copy link
Copy Markdown
Contributor

Two independent improvements to the sf push / sf sync upload path, one commit each.

1. Encrypt chunks with WebCrypto AES-GCM (~76× faster)

encryptFileChunk used @noble/ciphers gcm() at ~70 MiB/s. Because encryption is serialized with each part PUT, that caps upload throughput on any fast uplink. Switched the per-chunk file encryption to Node WebCrypto (subtle.encrypt AES-GCM), measured at ~5.3 GiB/s.

The output is ciphertext||tag with the same 16-byte tag, so ciphertexts, the SHA-1 digests Backblaze checks, and the HMAC upload proof are byte-for-byte identical. @noble stays for the once-per-file key wrap + filename metadata and for all decryption (which the server and apps also do with @noble).

Proof: the round-trip test decrypts the WebCrypto output with @noble, and a new test asserts WebCrypto and @noble produce identical bytes for the same key/nonce/plaintext (plus an empty-chunk case).

2. Retry a failed part instead of aborting the whole upload

putPartToBackblaze retried only on an expired token (401/403); any 5xx, timeout, or network blip threw and aborted the entire upload — a multi-gigabyte multipart would restart from byte zero. Added a bounded retry (up to 3 attempts, exponential backoff, a fresh part URL per retry) for 5xx / 408 / 429 / network errors. Part uploads are idempotent by (part number, SHA-1), so a retry safely overwrites the same part. The existing expired-token refresh path is unchanged.

Proof: a new test drives a 503 on part 1 and asserts it is retried with a refreshed URL and the upload completes; the existing token-refresh flow test still passes.

38/38 tests pass.

Not included (follow-up)

Pipelining/concurrent part uploads (2–4 parts in flight) is the remaining throughput lever but is a larger change — it needs multiple part URLs, interleaved retry handling, and a higher memory ceiling — on a multipart path that has not yet been run against production with a real large file. Better done deliberately with its own testing.

chogarcia added 2 commits July 3, 2026 00:26
…noble

encryptFileChunk used @noble/ciphers gcm() (~70 MiB/s), which caps upload
throughput on any reasonably fast uplink since encryption is serialized with
the part PUT. Switch the per-chunk encryption to Node WebCrypto
(subtle.encrypt AES-GCM, ~5 GiB/s). The output is ciphertext||tag with the
same 16-byte tag, so ciphertexts, the SHA-1 digests Backblaze checks, and the
HMAC upload proof are byte-for-byte unchanged.

@noble stays for the once-per-file key wrap and filename metadata, and for all
decryption (which the server and apps also do with @noble). encryptFileChunk
is now async; its only caller (encryptChunkWithDigest) was already async.

Tests: the round-trip test now decrypts the WebCrypto output with @noble, plus
a new test asserts WebCrypto and @noble produce identical bytes for the same
key/nonce/plaintext, and an empty-chunk case. 38/38 pass.
… upload

putPartToBackblaze retried only on an expired token (401/403); any 5xx,
timeout, or network blip threw and aborted the entire upload with no resume, so
a single transient failure forced a multi-gigabyte multipart restart from byte
zero. Add a bounded retry (up to 3 attempts, exponential backoff, a fresh
part URL per retry) for 5xx / 408 / 429 / network errors. Part uploads are
idempotent by (part number, SHA-1), so a retry safely overwrites the same part.
The existing expired-token refresh path is unchanged (it retries immediately
without consuming a backoff slot).

Tests: a new case drives a 503 on part 1 and asserts it is retried with a
refreshed URL and the upload completes; the existing token-refresh flow test
still passes. 38/38 pass.
@chogarcia
chogarcia merged commit 51eec60 into main Jul 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant