Reconnecting… Connection lost. Reload Session expired. Reload

← Back to Blog

Hash Functions Explained: MD5, SHA-256, bcrypt and When to Use Each

Checksums, password storage, Git commits, JWT signatures — hashing shows up everywhere. Here is a practical breakdown of the algorithms developers actually use, and the mistakes that keep showing up in code reviews.

By Pankaj Kumar Β· 11 Jun 2026 Β· 7 min read
πŸ‘€
Pankaj Kumar β€” Senior Software Engineer
8 years building .NET backend services with OWASP Top 10 as a daily checklist. Has personally migrated three legacy MD5-based password stores to bcrypt — the database diffs were educational.

The one thing all hash functions do

A hash function takes an input of any length and produces a fixed-size output β€” called a digest, hash, or checksum. That output is always the same length regardless of whether the input is three characters or three gigabytes. SHA-256 always produces 64 hex characters. MD5 always produces 32.

Two properties make them useful. First, the same input always produces the same output. Second, a tiny change in the input produces a completely different output β€” flip one bit and the resulting hash looks nothing like the original. This is called the avalanche effect, and it is what makes hashes useful for integrity checking.

The third property β€” and the one that matters most for security β€” is that you cannot go backwards. Given a hash, you cannot recover the original input. That makes them one-way functions, which is a different thing from encryption.

MD5 β€” still alive, mostly useless for security

MD5 was everywhere in the 2000s. If you opened any PHP codebase from that era you would find passwords stored as unsalted MD5 hashes. It produces a 32-character hex digest and runs extremely fast β€” which turns out to be the problem.

Researchers demonstrated practical collision attacks against MD5 in 2004: two different files can produce the exact same hash. By 2009, generating a collision took seconds on consumer hardware. If you have ever seen a download page list both a file and its "MD5 checksum," that checksum tells you almost nothing about whether the file was tampered with β€” a determined attacker can produce a malicious file that matches the same MD5.

Where MD5 is still fine: non-security fingerprinting, cache keys, database row change detection, or any use case where you just need a fast, consistent identifier and collisions are not an attack vector. Where it is not fine: anything touching passwords, certificates, or files you need to trust.

SHA-1 β€” the same story, slightly later

SHA-1 was meant to replace MD5. It produces a 40-character digest and was the default in SSL certificates, Git object IDs, and code signing for most of the 2000s and early 2010s. Google broke it publicly in 2017 with the SHAttered attack: two different PDF files with identical SHA-1 hashes, generated for $110,000 of compute time (now much cheaper).

Most certificate authorities stopped issuing SHA-1 certificates in 2017. Git still uses SHA-1 for object identifiers today, but the attack surface is different there β€” an attacker would need to control the repository to exploit it, and Git is slowly migrating to SHA-256. For anything new, there is no good reason to reach for SHA-1.

SHA-256 β€” the sensible default

SHA-256 is part of the SHA-2 family, designed by the NSA and published in 2001. No practical attack exists against it. It produces a 64-character hex digest and is fast enough for checksums while being secure enough for cryptographic use.

You encounter SHA-256 constantly: TLS certificate signatures, HMAC-SHA256 for JWT tokens (listed as HS256), Bitcoin proof-of-work, file integrity checks on software downloads, and most modern code signing. If someone asks you to "hash something for security purposes" and does not specify an algorithm, SHA-256 is the right answer.

SHA-512 is the same construction with a longer output. It is slightly slower on 32-bit hardware but faster on 64-bit CPUs β€” counterintuitively. The extra length rarely matters in practice. Use SHA-256 unless you have a specific reason for SHA-512.

Why you should never use SHA-256 to store passwords

This one surprises developers who are just getting into security. SHA-256 is a general-purpose hash β€” it is designed to be fast. Very fast. A modern GPU can compute several billion SHA-256 hashes per second. That speed is useful for checksums and completely disastrous for password storage.

The attack is simple: an attacker gets your database. They take a list of the 10 million most common passwords, hash every one of them with SHA-256, and compare against your stolen hashes. With a GPU doing billions of hashes per second, they can crack the majority of common passwords in minutes. Adding a salt (a random value mixed into the input before hashing) prevents precomputed lookup tables but does not meaningfully slow down targeted brute-force attacks.

Password hashing algorithms solve this by being intentionally, tuneably slow:

  • bcrypt has a cost factor you control. Set it so that hashing one password takes around 250–300 ms on your server. That is fine for a login endpoint, but it turns a GPU attack from "minutes" to "years." bcrypt has been around since 1999 and is supported everywhere. It is the safe, boring choice.
  • Argon2id is newer (won the Password Hashing Competition in 2015) and adds memory-hardness on top of time-hardness, making GPU and ASIC attacks even less practical. OWASP recommends it as the first choice for new systems. If your framework supports it, use this.
  • scrypt is similar to Argon2 in its memory-hard properties. Less widely available in framework libraries than bcrypt but more future-resistant than MD5/SHA-based approaches.

All three of these handle salting internally. You do not generate a salt, store it separately, or combine it with the password yourself. You call the library, pass the password, and it returns a self-contained string that includes the algorithm, cost parameters, salt, and hash together.

HMACs β€” when you need to prove a message came from you

A plain hash answers: "Has this data changed?" An HMAC (Hash-based Message Authentication Code) answers: "Did someone with the right secret key produce this?" It combines a hash with a shared secret.

You see HMACs in a few places. JWT tokens signed with HS256 are HMAC-SHA256 under the hood β€” the server signs the token with a secret key, and any other server with the same secret can verify it without calling a central auth service. Stripe, GitHub, and most payment providers use HMACs to sign webhook payloads: they compute HMAC-SHA256 of the request body using a shared secret, and you recompute it on your end to verify the webhook actually came from them and was not modified in transit.

Picking the right algorithm

Honestly, for most developers the decision is pretty simple:

  • Storing a password? Use bcrypt (or Argon2id if your stack supports it). Never anything else.
  • Verifying a file download or generating a checksum? Use SHA-256.
  • Signing a JWT or a webhook? Use HMAC-SHA256.
  • Building a non-security cache key or fingerprint? MD5 is fine, though SHA-256 is not meaningfully slower for this.
  • Anything involving SHA-1 or MD5 in a security context? Treat it as broken and migrate away.

The decision only gets complicated when you are building cryptographic infrastructure, working on compliance requirements, or operating in a post-quantum context. For typical application code, stick with SHA-256 and bcrypt and you will be fine.

Try the free tool
Hash Generator

Generate MD5, SHA-1, SHA-256, SHA-512, and other hashes from any text — instantly, in your browser, with no data sent to a server.

Open Hash Generator