SHA-256 shows up everywhere: Git commits, TLS certificates, blockchain, file integrity checks, API signatures. Most developers use it indirectly a hundred times a day without thinking about it. But when you actually need to reach for it directly, it helps to know what it does and — just as important — what it doesn't do.
What SHA-256 actually is
SHA-256 is a cryptographic hash function from the SHA-2 family, standardised by NIST. You feed it any amount of data and it spits out a fixed 256-bit value, usually written as 64 hexadecimal characters. That output is called a digest.
Three properties make it useful:
- Deterministic: the same input always produces the same digest.
- Fixed size: a one-byte file and a 4 GB video both hash to 64 hex chars.
- One-way and collision-resistant: you can't practically reverse a digest back to the input, and finding two inputs with the same digest is computationally infeasible.
Change a single bit of input and the whole output changes unpredictably. That avalanche effect is exactly what makes it good for detecting tampering.
When you'd reach for it
File and data integrity
The classic use. Publish a file alongside its SHA-256 digest, and anyone who downloads it can hash their copy and compare. If the digests match, the bytes are identical. Linux distributions do this for ISO images all the time.
Deduplication and content addressing
Git uses hashing to identify objects by their content rather than a filename. If two files have the same digest, you store the blob once. Content-addressable storage systems lean on this heavily.
Signatures and verification
Digital signatures typically sign a hash of the message, not the message itself. HMAC-SHA256 is the workhorse for verifying webhook payloads — Stripe, GitHub, and plenty of others sign requests this way so you can confirm they weren't forged or altered in transit.
Idempotency keys and cache keys
Need a stable, compact identifier for some structured input? Hashing a canonical serialisation of it gives you a fixed-length key that's safe to use in URLs, filenames, or cache lookups.
Where SHA-256 is the wrong tool
This is where people get burned. SHA-256 is fast, and for password storage that's a bug, not a feature.
If you hash user passwords with plain SHA-256, an attacker who steals your database can try billions of guesses per second on a GPU. Use a purpose-built password hash instead — bcrypt, scrypt, or Argon2 — which are deliberately slow and salted. SHA-256 has its place, but the login table isn't it.
Similarly, don't roll your own HMAC by concatenating a secret and a message and hashing the result. Use a real HMAC implementation. The naive approach is vulnerable to length-extension attacks, which SHA-256 is susceptible to.
Common gotchas
- Encoding matters. Hashing the string "café" gives different results depending on whether it's UTF-8 or UTF-16, and whether the é is a single code point or a combining accent. Normalise your input encoding before hashing, always.
- Whitespace and line endings. A trailing newline or a Windows CRLF versus a Unix LF changes the digest completely. If you're comparing hashes across systems, agree on canonical formatting first.
- Hex vs Base64 vs raw bytes. The same digest can be represented multiple ways. Comparing a hex string to a Base64 string will never match even when the underlying bytes are identical.
- Timing attacks. When comparing a computed digest against an expected one for security purposes, use a constant-time comparison, not a plain
==.
A quick worked example
Here's the same operation in a few languages so you can see how little code it takes.
Node.js:
const crypto = require('crypto');
const digest = crypto
.createHash('sha256')
.update('hello world', 'utf8')
.digest('hex');
console.log(digest);
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9Python:
import hashlib
digest = hashlib.sha256('hello world'.encode('utf-8')).hexdigest()
print(digest)
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9Go:
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
sum := sha256.Sum256([]byte("hello world"))
fmt.Printf("%x\n", sum)
}
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9Notice all three produce the identical digest for the same input. That's the whole point — it's a stable fingerprint you can compute anywhere.
When I just need to check a value quickly — verify a downloaded file matches a published hash, or sanity-check that my code produces the digest I expect — I don't bother writing a script. I paste it into an online SHA-256 Hash Generator and compare. It's faster than opening a REPL, and it's a handy way to confirm your encoding assumptions match reality.
Practical rules to keep in mind
- Use SHA-256 for integrity, fingerprinting, and content addressing — that's what it's built for.
- Don't use raw SHA-256 for passwords. Reach for Argon2, bcrypt, or scrypt.
- Use a library's HMAC for message authentication, never hand-rolled hashing.
- Pin down encoding, whitespace, and representation before comparing digests across systems.
- Compare security-sensitive digests in constant time.
SHA-256 is one of those primitives that's boring in the best possible way. It does exactly one thing, it does it consistently across every platform, and it's fast enough to hash gigabytes without complaint. Learn its edges — mostly the encoding traps and the places it's the wrong choice — and it becomes a reliable tool you'll keep coming back to.
Photo by Chris Ried on Unsplash