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)
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9Notice 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.isoCompare 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