Web Crypto SHA-256: Large-File Memory Limits
Front-end upload and deduplication flows often need a file fingerprint. Native crypto.subtle.digest supports SHA-256 but has no .update() method: the complete input must be in memory before the call. Blob.slice() alone cannot turn it into streaming hashing. This guide separates a size-bounded native path from the incremental Worker architecture required for genuinely large files.
1. Problem: peak memory and interaction risks during file hashing
Calling file.arrayBuffer() on a multi-gigabyte file allocates another contiguous buffer roughly as large as the file and may fail, trigger process termination, or create severe memory pressure. The fact that subtle.digest() returns a Promise does not eliminate CPU and resource contention, so explicit size policy and device testing are still required.
2. Minimal reproduction: indiscriminate file loading and Web Crypto limits
This snippet illustrates common pitfalls when attempting full file reads and confusing SubtleCrypto.digest algorithm support:
/* Defect 1: attempting MD5 with SubtleCrypto -> throws DOMException (Algorithm Not Supported) */
// await window.crypto.subtle.digest("MD5", data); // SubtleCrypto does NOT support MD5!
/* Defect 2: loading a 2GB file entirely into ArrayBuffer -> OOM browser crash! */
// const buffer = await file.arrayBuffer(); // Dangerous!3. Root cause: SubtleCrypto algorithm and input-model limits
1. Algorithm limits: subtle.digest() supports SHA-1, SHA-256, SHA-384, and SHA-512; MD5 is omitted. 2. No incremental API: one call accepts a complete BufferSource and cannot update one digest state chunk by chunk. 3. Memory boundary: the file buffer, browser-internal processing, and the rest of the page contribute to peak memory. No fixed per-browser threshold is guaranteed by the Web Crypto specification.
4. Recommendation: choose the implementation by file size
1. Small and medium files: enforce a configurable product limit, then use file.arrayBuffer() and crypto.subtle.digest('SHA-256', buffer). 2. Genuinely large files: use a vetted incremental SHA-256 JS/Wasm implementation inside a Web Worker, read chunks sequentially, and update one hash state. Never hash chunks independently and concatenate their digests. 3. Progress and cancellation: the main thread should receive only progress and the final digest and must be able to terminate the Worker. A Worker isolates CPU work but does not remove memory requirements.
5. Complete code: size-bounded native Web Crypto SHA-256
This code states the native API boundary honestly: it accepts files only below an explicit limit and reads the full input once. 64 MiB is an example product policy, not a browser guarantee; calibrate it across supported devices. Route larger files to an incremental Worker implementation.
const DEFAULT_NATIVE_DIGEST_LIMIT = 64 * 1024 * 1024;
async function computeFileSha256WithWebCrypto(
file: File,
maxBytes = DEFAULT_NATIVE_DIGEST_LIMIT,
): Promise<string> {
if (file.size > maxBytes) {
throw new RangeError(
"File exceeds the native digest memory policy; use incremental SHA-256 in a Worker",
);
}
const bytes = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
}6. Incorrect approaches
Calling SubtleCrypto.digest('MD5', buffer) expecting MD5, resulting in unhandled DOMException errors. Blocking the UI main thread with endless synchronous hash loops. Loading entire multi-gigabyte files without checking size bounds.
7. Boundaries: device variance, Workers, and buffer ownership
Do not rely on a fixed memory threshold that no browser specification guarantees; available memory varies by device, browser version, and concurrent tabs. Transfer chunk buffers to the Worker to reduce copying, but verify that the incremental library does not retain every chunk. Cancellation, navigation, and Worker errors must release resources.
8. Verification
Compare browser output with shasum -a 256 or a server-side SHA-256 implementation. Fix test vectors for empty files, non-aligned sizes, zero bytes, and over-limit inputs. For the Worker path, measure peak memory, main-thread long tasks, cancellation behavior, and monotonic progress across file sizes.
9. FAQ
Can I call subtle.digest() on every Blob.slice() and concatenate the results? No. That produces independent digests and is not the SHA-256 of the complete file.
How should a 2GB file be handled? Use a trusted incremental SHA-256 JS/Wasm implementation in a Worker, feeding chunks into one state while validating supply chain, licensing, and standard test vectors.
Does a Worker prevent OOM? No. It isolates computation, but you must still bound chunks, avoid retaining all input, and measure peak memory.
10. Summary
Native Web Crypto digest() is non-streaming. Use it only below an explicit memory policy; for large files, use a Worker plus a vetted incremental hash implementation and verify it with standard vectors and resource measurements. Empty chunk loops are not streaming SHA-256.
Sources and further reading
Specifications and references for checking the article’s technical details.
Related articles
MD5 Hash Mismatches: UTF-8 vs GBK Encoding
Explain how hash functions process raw byte streams rather than abstract characters, detailing UTF-8 vs. GBK encoding mismatches and resolution strategies.
Best practicesPBKDF2 Password Hashing vs Plain SHA-256
Understand why plain SHA-256 password hashes are fast to crack, and how unique salts plus PBKDF2 slow offline guessing.
How it worksJS Obfuscation: Control Flow and Dead Code Costs
Analyze Control Flow Flattening dispatchers and Dead Code Injection mechanisms, evaluating impacts on CPU execution overhead, bundle size, and debugging.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool