What Actually Happens When You Hash a File in Your Browser?

Published on May 15, 2026 by The Kestrel Tools Team • 8 min read

You drop a 200 MB ISO into a download verifier, the page spins for two seconds, and a SHA-256 hex string appears. Did the file just travel across the internet, get hashed on someone’s server, and come back? Or did your browser do all of it without ever opening a socket?

For most online hash generators, the answer is the first one. For a small number — including the one we’ll walk through here — it’s the second. The difference matters, and once you’ve seen the actual code, you’ll never confuse the two again.

This post is a practical walkthrough of how to hash a file in browser JavaScript using the Web Crypto API. We’ll cover what crypto.subtle.digest actually does, the four-line FileReader glue that holds it together, the gotchas that bite people on large files, and how to verify the result against your local shasum or sha256sum. By the end, you’ll be able to read a client-side Hash Generator and recognize whether it’s the real thing.

How do you hash a file in browser JavaScript?

You read the file’s bytes with the File API, pass the resulting ArrayBuffer to crypto.subtle.digest, and convert the returned hash buffer to a hex string. It is six lines of code and uses no third-party library. Here is the minimum working version:

async function hashFile(file) {
  const buffer = await file.arrayBuffer();
  const digest = await crypto.subtle.digest('SHA-256', buffer);
  return [...new Uint8Array(digest)]
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

Wired up to an <input type="file">, that’s a complete client-side hash generator. The browser never sends a network request. The file’s bytes never leave the tab. The hex output you get back is byte-for-byte identical to what sha256sum file.iso produces in a terminal — and you can verify that yourself in 30 seconds.

What crypto.subtle.digest is actually doing

The Web Crypto API exposes crypto.subtle.digest(algorithm, data) as a one-shot hashing primitive. Four algorithms are supported in every modern browser: SHA-1, SHA-256, SHA-384, and SHA-512. (MD5 is deliberately not included; if you need MD5 for legacy checksum compatibility, you have to ship a JS implementation yourself.)

Underneath, browser engines route this call into native code:

  • Chromium uses BoringSSL.
  • Firefox uses NSS.
  • WebKit uses CommonCrypto on Apple platforms.

This is the same set of audited C/C++ implementations that power the browser’s TLS stack, your password manager’s key derivation, and Service Worker integrity checks. When you call crypto.subtle.digest('SHA-256', buffer), you are running the same SHA-256 implementation that Chrome uses to verify a TLS certificate. That’s a useful fact to keep in mind when someone suggests pulling in a 14 KB minified js-sha256 polyfill instead.

The call returns a Promise<ArrayBuffer>. That’s because hashing 200 MB of data is non-trivial work, and digest is allowed to do it on a worker thread without blocking the main thread. In practice, browsers do exactly that for inputs over a few MB.

The FileReader vs file.arrayBuffer() question

If you’ve copied browser-hashing snippets from older Stack Overflow answers, you have probably seen FileReader everywhere:

const reader = new FileReader();
reader.onload = async (e) => {
  const digest = await crypto.subtle.digest('SHA-256', e.target.result);
  // ...
};
reader.readAsArrayBuffer(file);

This still works, but it’s been unnecessary since 2020. Every File and Blob now has an arrayBuffer() method that returns a promise — no event-handler dance required. The shorter version reads cleaner and integrates with async/await without a custom Promise wrapper:

const buffer = await file.arrayBuffer();

Use arrayBuffer() for new code. Reserve FileReader for the one case it still solves better, which is reading a file as a DataURL for an <img> preview.

What happens with a 2 GB file

The code above breaks the moment someone uploads a video file. file.arrayBuffer() loads the entire file into memory before returning. On a 2 GB file, that’s 2 GB of RAM — and crypto.subtle.digest then needs another contiguous buffer to operate on. On a laptop with 16 GB total, you’ll OOM the tab.

To hash large files in the browser, you need a streaming approach. As of 2025, browsers expose Blob.stream(), which returns a ReadableStream of Uint8Array chunks. The crypto.subtle.digest primitive itself isn’t streaming-aware (it always wants the full buffer), so you have two real options:

  1. Pipe the stream through a hashing transform, like the @noble/hashes library’s incremental SHA-256, which accepts chunks. Adds about 8 KB to your bundle but works on any file size up to the browser’s max disk-backed Blob limit.
  2. Wait for the streaming Web Crypto proposal, which has been on the W3C agenda since 2023 but isn’t shipped anywhere yet.

For files under ~500 MB, the simple arrayBuffer() + digest pattern works fine on any modern device. Above that, switch to streaming. A pragmatic threshold most production code uses is 256 MB.

Verifying your in-browser hash matches sha256sum

This is the single best sanity check that a client-side hash generator is doing what it claims. Pick any file:

# macOS / Linux
shasum -a 256 sample.txt
# Output: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  sample.txt

Now drop the same file into a client-side Hash Generator and select SHA-256. The hex strings should match exactly, character for character. If they don’t, either the tool is hashing something other than the raw bytes (a common bug: hashing the base64-encoded form instead of the binary), or it’s compressing/normalizing the file before hashing — neither of which is acceptable for an integrity check.

This test is also how you can tell whether a tool is genuinely client-side. Open DevTools, switch to the Network tab, drop the file in, and watch. A real client-side hash generator shows zero outbound requests. A server-side one fires a POST (or worse, a multipart upload) and your file is now sitting in someone’s request log.

Handling the user-experience details

The hashing code is the easy part. Shipping a hash generator that doesn’t feel janky on a real user’s hardware needs a few extra moves:

  • Show progress. For files over a few MB, hashing isn’t instant. Wrap the call in a useState + setLoading(true) pattern (or whatever your framework’s equivalent is) and surface a spinner.
  • Run on a Web Worker for big files. crypto.subtle.digest releases the main thread internally, but if you’re also doing the hex conversion or any UI work afterwards, the post-hash render can stutter. Worker isolation keeps the input field responsive.
  • Surface multiple algorithms at once. Most users who hash files want to compare against a published checksum. Sometimes the upstream publishes SHA-256, sometimes SHA-1, sometimes MD5. Computing all four (where supported) in parallel is cheaper than re-reading the file each time.
  • Format the output for copy-paste. Lowercase hex, no spaces, no 0x prefix — that’s the format that matches every CLI tool’s output and pastes cleanly into a diff or a verification script.

A complete client-side hash generator in 40 lines

Putting all of this together, here’s a self-contained hash generator that handles modest file sizes correctly:

<input type="file" id="file-input" />
<pre id="output"></pre>

<script type="module">
  const ALGORITHMS = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'];

  async function hashFile(file, algo) {
    const buffer = await file.arrayBuffer();
    const digest = await crypto.subtle.digest(algo, buffer);
    return [...new Uint8Array(digest)]
      .map((b) => b.toString(16).padStart(2, '0'))
      .join('');
  }

  document.getElementById('file-input').addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    const out = document.getElementById('output');
    out.textContent = `Hashing ${file.name} (${file.size} bytes)...`;
    const results = await Promise.all(
      ALGORITHMS.map(async (algo) => `${algo}: ${await hashFile(file, algo)}`)
    );
    out.textContent = results.join('\n');
  });
</script>

Drop that into an HTML file, open it locally, and you have a fully working hash generator that runs entirely in your browser. No server, no backend, no library. The whole thing is under a kilobyte.

Why client-side hashing isn’t a privacy stunt

It’s tempting to read “runs in your browser” as a privacy talking point — and it is one — but the technical case is at least as strong as the privacy one.

Speed. A 100 MB file hashed locally on an M-series MacBook completes in under a second. The same file uploaded to a server-side hash tool requires a round trip that takes 5–30 seconds depending on connection and chunk size. The local version is faster even before you factor in queue time on the remote.

Reliability. Local hashing works on a flight, on a train, behind a corporate firewall, and on the engineer’s laptop with no internet at all. Server-side hashing does not.

Correctness. When the hash function runs on the same machine the file lives on, there’s no chance of in-transit corruption confusing the result. With server-side hashing, a partial upload that the server doesn’t notice produces a hash for part of your file — and that hash matches nothing.

Auditability. You can read the source of a client-side hash generator in your DevTools. You cannot read the source of a server-side one. For something whose entire job is integrity verification, that audit gap is awkward.

At Kestrel Tools, our Hash Generator runs entirely in the browser using exactly the crypto.subtle.digest pattern in this post — same SHA-256 implementation Chromium uses for TLS, no upload, no log. You can verify it with the DevTools network-tab trick in 30 seconds, which is more than most servers can offer.

The takeaway

Hashing a file in browser JavaScript isn’t a clever trick or a privacy gesture. It’s a six-line function backed by the same audited cryptographic code that powers your browser’s TLS stack. crypto.subtle.digest is fast, available everywhere, and produces output that’s bit-identical to sha256sum.

The one nuance is large files: stick with file.arrayBuffer() under 256 MB, switch to a streaming hash for anything bigger. Beyond that, the implementation is short enough to read in one sitting and easy enough to verify against any CLI checksum tool.

If you’d rather skip the implementation and just hash a file right now, Kestrel Tools’ Hash Generator is a free client-side hash generator that supports SHA-1, SHA-256, SHA-384, and SHA-512 in parallel — drop a file in, get four hashes out, no upload, no waiting.