SHA-256 shows up everywhere: Git commits, TLS certificates, blockchain, file integrity checks, and about a thousand tutorials that get it slightly wrong. It's one of those primitives you'll use for your entire career, so it's worth understanding properly rather than copy-pasting from Stack Overflow and hoping.

What SHA-256 actually is

SHA-256 is a cryptographic hash function from the SHA-2 family, designed by the NSA and published by NIST. It takes any input — a byte, a book, a 4GB video file — and produces a fixed 256-bit output, usually written as 64 hexadecimal characters.

The important properties:

  • Deterministic: the same input always gives the same output.
  • One-way: you can't reverse a hash back into its input.
  • Avalanche effect: change one bit and roughly half the output bits flip.
  • Collision resistant: finding two inputs with the same hash is computationally infeasible.

That last point matters. A hash is not encryption. There's no key, and there's no "decrypt" step. If someone talks about decrypting a SHA-256 hash, they're confused.

When you'd reach for it

Verifying integrity

This is the bread-and-butter use case. You download a Linux ISO, the site publishes a SHA-256 checksum, you hash your file and compare. If the two strings match, the file arrived intact. If they don't, something changed — a corrupted download, a truncated transfer, or worse, tampering.

Deduplication and content addressing

Git identifies objects by their hash. Content-addressable storage systems use the hash of the data as its key, so identical content is stored once. If you're building a cache or an asset pipeline, hashing content is a clean way to detect duplicates without comparing bytes.

Signatures and HMAC

SHA-256 is the workhorse underneath digital signatures and message authentication codes (HMAC-SHA256). When an API signs a webhook payload so you can verify it came from them, that's almost always HMAC with SHA-256.

When NOT to use it

Here's the mistake I see most often: hashing user passwords with plain SHA-256.

SHA-256 is fast. That's a feature for integrity checks and a disaster for passwords, because it means an attacker with a leaked database can try billions of guesses per second on a GPU. For passwords you want a deliberately slow, salted algorithm — bcrypt, scrypt, or Argon2. Use those. SHA-256 alone is not password storage.

Common gotchas

Encoding matters

A hash is computed over bytes, not characters. The string "café" hashes differently depending on whether you encode it as UTF-8 or Latin-1. If two systems produce different hashes for the "same" text, encoding is the first thing to check. Always be explicit about UTF-8.

Hex vs Base64 vs raw bytes

The same hash can be displayed as a 64-character hex string, a Base64 string, or 32 raw bytes. These aren't different hashes — just different representations. Comparison failures between systems are often just format mismatches.

Timing attacks on comparison

When you compare a computed HMAC against one from a request, don't use a normal ==. A naive comparison returns early on the first mismatched byte, and that timing difference can leak information. Use a constant-time comparison function like crypto.timingSafeEqual in Node or hmac.compare_digest in Python.

Trailing whitespace and newlines

A file with a trailing newline hashes differently from one without. When integrity checks mysteriously fail, an editor that added a newline on save is a classic culprit.

A short worked example

Here's the same input hashed in Node.js and Python. Both should give you the identical result:

// Node.js
const crypto = require('crypto');

const hash = crypto
  .createHash('sha256')
  .update('hello world', 'utf8')
  .digest('hex');

console.log(hash);
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
# Python
import hashlib

h = hashlib.sha256('hello world'.encode('utf-8')).hexdigest()
print(h)
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

Notice both explicitly say UTF-8, and both output hex. If you run these and get that exact 64-character string, your setup is behaving. If you want to sanity-check a value quickly without opening a REPL, our SHA-256 Hash Generator gives you the same result in the browser — handy when you're debugging a checksum mismatch and just need a known-good reference.

Verifying a file on the command line

On macOS and Linux you don't even need code:

shasum -a 256 ubuntu.iso
# or
sha256sum ubuntu.iso

Compare the output against the published checksum. On Windows, certutil -hashfile ubuntu.iso SHA256 does the same job.

HMAC, done right

If you're verifying a webhook, you want HMAC rather than a bare hash, because a plain hash can be forged by anyone who knows the message format. HMAC mixes in a shared secret:

const crypto = require('crypto');

function verify(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

That length check before timingSafeEqual matters — the function throws if the buffers differ in length.

Is SHA-256 still safe?

Yes. Unlike MD5 and SHA-1, which are broken and should not be used for anything security-sensitive, SHA-256 has no practical collision attacks. It's the sensible default for general-purpose hashing today. If you need more, SHA-512 or SHA-3 exist, but for the vast majority of work SHA-256 is exactly right.

The rules to keep in your head: it's a fingerprint, not a secret store. Be explicit about encoding. Compare hashes in constant time. Reach for Argon2 when passwords are involved. Get those four right and you'll avoid nearly every SHA-256 mistake I've watched people make.

Photo by Chris Ried on Unsplash