Type “HMAC vs SHA-256” into a search bar and you’ll find a wall of half-answers, mostly from Q&A threads where someone half-remembers a security course from 2015. That gap matters more than it looks. Developers wire webhook verification, JWT signing, and API request auth into production every week, and a surprising number of them use plain SHA-256 where they needed HMAC-SHA256, or bolt together a homemade “SHA256(secret + message)” scheme that a length-extension attack can break in minutes. This piece lays out exactly where SHA-256 ends and HMAC-SHA256 begins, with real benchmark numbers, real cloud pricing, and real examples from Stripe, GitHub, Shopify, and AWS.

The short version: SHA-256 is a hash function. HMAC-SHA256 is an authentication scheme built on top of a hash function, using a secret key. They are not competitors and you rarely choose one over the other in the way you’d choose between two databases. But conflating them is one of the most common cryptographic mistakes engineers make, and it has caused real vulnerabilities. This comparison exists to end that confusion, with data, not just definitions.

What SHA-256 Actually Is

SHA-256 belongs to the SHA-2 family of cryptographic hash functions, standardized by NIST in FIPS 180-4. It takes an input of any length and always returns a fixed 256-bit (32-byte) digest, usually shown as 64 hexadecimal characters. Feed it a single character or a 4GB file, the output is the same size either time. Change one bit of the input and roughly half the output bits flip, a property called the avalanche effect.

SHA-256 processes data in 512-bit blocks using a Merkle–Damgård construction, running each block through 64 rounds of bitwise operations, modular additions, and compression functions. It has no concept of a secret. Anyone with the input can compute the same output, every time, deterministically. That’s the entire point: SHA-256 proves a file or message hasn’t changed. It does not prove who sent it.

This is where a lot of confusion starts. Git uses SHA-256 (in newer repository formats, SHA-1 in legacy ones) to identify commits and objects by content, not to authenticate who pushed them. Software vendors publish a SHA-256 checksum next to a download so you can confirm the file wasn’t corrupted or tampered with in transit, assuming you fetch the checksum over a trusted channel. Neither use case involves a secret key, and neither one is really an authentication mechanism in the cryptographic sense, even though people often describe checksum verification loosely as “verifying” a file.

What HMAC Is, and Why It Exists

HMAC stands for Keyed-Hash Message Authentication Code. It’s defined in RFC 2104 and further specified for FIPS-approved use in NIST’s guidance. HMAC combines a secret key with a hash function, commonly SHA-256, to produce a tag that proves two things at once: the message wasn’t altered, and it was generated by someone who possesses the shared secret.

The construction looks like this: HMAC(K, m) = H((K XOR opad) || H((K XOR ipad) || m)). In plain terms, HMAC runs the hash function twice, once over an “inner” combination of the key and message, and again over an “outer” combination of the key and that first result. This double-pass design isn’t decoration. It’s specifically built to close a real vulnerability that shows up when engineers try to build authentication out of a hash function on their own.

The Core Difference: Authentication vs Integrity

Here’s the distinction that trips people up. SHA-256 alone gives you integrity against accidental corruption: if a file’s hash matches what you expected, the bytes are almost certainly unchanged. But it gives you zero protection against a deliberate attacker, because anyone can compute a fresh SHA-256 hash for a tampered file and swap both in together. There’s no secret standing in the way.

HMAC-SHA256 solves that by requiring a shared secret only the legitimate sender and receiver hold. An attacker who intercepts a message and its HMAC tag can’t forge a new tag for a modified message, because they don’t have the key. This is why every webhook system worth using signs its payloads with HMAC, not raw SHA-256. Stripe, GitHub, and Shopify all rely on it, and we’ll get into their exact implementations below.

One rule of thumb that holds up well in practice: reach for SHA-256 alone when you only need to detect accidental corruption and trust isn’t in question (checksums, content-addressed storage, deduplication). Reach for HMAC-SHA256 the moment you need to prove a message came from a specific party holding a secret, which covers the vast majority of API and webhook security work.

The Length-Extension Attack Nobody Talks About

This is the vulnerability that HMAC was specifically designed to prevent, and it still catches developers who try to roll their own MAC. Because SHA-256 uses a Merkle–Damgård construction, an attacker who knows H(message) but not the message itself can compute H(message || extra_data || padding) for any extra_data they choose, without ever knowing the original message or any secret key involved. That sounds abstract until you see the naive mistake that triggers it: constructing an authentication scheme as SHA256(secret_key + message) instead of using HMAC.

With that naive construction, an attacker can take a valid hash, extend the message with attacker-controlled data, and produce a new valid hash for the extended message, all without knowing the secret key. It’s a real class of bug, not a theoretical one. Length-extension issues have shown up in the wild in poorly designed API signing schemes. HMAC’s inner/outer double-hash structure specifically blocks this attack path. That’s the whole reason the construction is more complex than “just hash the key and the message together.”

SHA-256 vs HMAC-SHA256: Full Specification Comparison

The table below lines up the two side by side across the properties engineers actually need to check before building something on top of either one.

PropertySHA-256HMAC-SHA256
TypeCryptographic hash functionKeyed-hash message authentication code
StandardNIST FIPS 180-4RFC 2104 / NIST FIPS 198-1
Requires a secret keyNoYes
Output size256 bits (32 bytes)256 bits (32 bytes, using SHA-256 as the base)
Input block size512 bits512 bits (from underlying SHA-256)
Provides authenticationNoYes
Provides integrityYes (against accidental change)Yes (against tampering by non-key-holders)
Vulnerable to length-extension attacksYes, when misused as SHA256(key+message)No, by design
Hash passes per operation12 (inner and outer)
Relative CPU costBaselineRoughly 1.8x-2.2x SHA-256 alone
Common use casesChecksums, git object hashing, content-addressed storageWebhook verification, JWT HS256, API request signing
Needs secret management infrastructureNoYes (key storage, rotation, distribution)

Performance Benchmarks: How Much Slower Is HMAC?

Because HMAC-SHA256 runs the underlying hash function roughly twice per message (once for the inner pass, once for the outer), it costs more CPU cycles than a single SHA-256 call. The exact multiplier depends heavily on hardware and whether the CPU has dedicated hash acceleration.

On CPUs with Intel’s SHA-NI extensions, one independent benchmark measured plain SHA-256 running at roughly 1.8 cycles per byte, against about 7.7 cycles per byte for a software-only OpenSSL implementation without the extension, a gap of roughly 4.2x. On an Intel Core i7 test, a single SHA-256 call landed around 901 cycles without hardware acceleration and dropped to about 270 cycles with SHA-NI enabled for a single buffer. Separately, a modern Intel N100 system reached about 1.54 GB/s of SHA-256 throughput at large block sizes in OpenSSL’s own speed benchmark, and other measurements put SHA-NI-accelerated throughput above 2 GB/s on a single core for large inputs.

None of those SHA-NI gains are specific to HMAC. SHA-NI accelerates the underlying SHA-256 compression function, and HMAC just calls that function twice per message (plus key padding overhead), so the speedup carries over proportionally. Because HMAC performs two hash passes and a small amount of key setup, the realistic overhead versus a single SHA-256 call sits around 1.8x to 2.2x the cycles per byte, close to a straight doubling, though well-optimized implementations that precompute the inner and outer key pads shave a bit off that for long messages. OpenSSL’s own benchmarking tool, run as openssl speed -hmac sha256, reports operations-per-second across a range of block sizes if you want to measure this on your own hardware rather than trust a blog’s numbers.

Benchmark SourceSHA-256 AloneCondition
Peer-reviewed SHA-NI evaluation1.8 cycles/byteIntel SHA-NI hardware acceleration
Same evaluation, software fallback7.7 cycles/byteOpenSSL software-only, no SHA-NI (4.2x slower)
Intel Core i7 single-call test901 cycles (no ext.) / 270 cycles (SHA-NI)Single buffer hash call
Modern core, large input2+ GB/s throughputSHA-NI, large message
Intel N100, OpenSSL speed test~1.54 GB/sLargest tested block size
3rd-gen Intel Core, AVX-optimized10.84 cycles/bytePre-SHA-NI AVX optimization work

For nearly every real application, this overhead is irrelevant. Signing a JSON webhook payload or a JWT header takes microseconds either way. The performance question only starts to matter at extreme scale, such as validating millions of API signatures per second, where doubling the hash work can genuinely move a capacity-planning number.

HMAC-SHA256 vs Other HMAC Variants

HMAC isn’t locked to SHA-256. It’s a construction that wraps around whatever hash function you plug in, and the choice of hash changes both the output size and the security margin. Picking the wrong variant is a separate mistake from confusing HMAC with plain SHA-256, but it’s common enough to cover here.

HMAC-SHA1 still shows up in legacy systems, including older versions of Twilio’s request-signing scheme and some OAuth 1.0a implementations. SHA-1 itself is considered cryptographically broken for collision resistance, with a practical collision demonstrated by Google and CWI Amsterdam back in 2017, but HMAC-SHA1’s security as a MAC doesn’t rely on collision resistance the same way a standalone hash does, since the attack surface is different when a secret key is involved. Even so, most current guidance recommends migrating off HMAC-SHA1 where a newer option is available, mainly to avoid maintaining two different security postures for the same primitive family.

HMAC-SHA512 trades a larger 512-bit output and typically better performance on 64-bit hardware (since SHA-512 operates on 64-bit words) for larger signature headers on the wire. HMAC-SHA3-256, built on the different Keccak-based construction behind SHA-3, sidesteps the entire length-extension attack class by design, since SHA-3 isn’t a Merkle–Damgård hash, though it sees far less real-world adoption than the SHA-2-based options because tooling and library support lag behind.

HMAC VariantUnderlying HashOutput SizeTypical Use Today
HMAC-SHA1SHA-1160 bitsLegacy systems, OAuth 1.0a, being phased out
HMAC-SHA256SHA-256256 bitsDefault choice for webhooks, JWT HS256, API signing
HMAC-SHA384SHA-384384 bitsHigher-assurance TLS cipher suites, some enterprise auth
HMAC-SHA512SHA-512512 bitsSystems wanting a larger margin or 64-bit performance edge
HMAC-SHA3-256SHA3-256 (Keccak)256 bitsNiche; avoids Merkle–Damgård entirely, limited library support

For nearly every new project, HMAC-SHA256 remains the practical default. It has the widest library support, the most documentation, and it’s what every major API and webhook provider already expects. Stepping up to HMAC-SHA512 makes sense mainly when you’re already standardized on SHA-512 elsewhere in a system, not as a general security upgrade, since HMAC-SHA256 has no known practical weakness that HMAC-SHA512 closes.

Verifying a Webhook Signature: A Worked Example

Here’s what a correct HMAC-SHA256 webhook verification function looks like in Node.js, following the pattern Stripe, GitHub, and Shopify all document. Note the constant-time comparison at the end, which is the step most homemade implementations get wrong.

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, receivedSignature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');

  const expectedBuffer = Buffer.from(expectedSignature, 'hex');
  const receivedBuffer = Buffer.from(receivedSignature, 'hex');

  if (expectedBuffer.length !== receivedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
}

Three details in that function matter more than they look. First, the HMAC is computed over the raw, unparsed request body, exactly as it arrived on the wire, not a JSON.stringify of a parsed object. Second, the length check before the comparison exists because crypto.timingSafeEqual throws if the two buffers aren’t the same length, rather than returning false, so you have to guard against that explicitly. Third, the secret itself should come from a secrets manager or environment variable injected at deploy time, never hardcoded in source, since a leaked HMAC secret is just as dangerous as a leaked API key.

The Cost of Managing HMAC Keys in the Cloud

This is the part most comparisons skip, and it’s arguably more important than raw CPU cycles for a production decision. SHA-256 alone needs zero secret-management infrastructure, because there’s no secret. HMAC needs a key, and that key has to be generated, stored, rotated, and protected, which is where managed key services and their pricing enter the picture.

All three major clouds now support HMAC keys as first-class citizens in their key management services, and all three charge for it, on top of whatever compute you’re already paying for. Here’s what that actually costs as of late 2026.

ProviderPer-Key Monthly CostPer-Operation CostNotes
AWS KMS$1.00/month per HMAC key$0.03 per 10,000 GenerateMac/VerifyMac calls20,000 free requests/month across most symmetric operations
Azure Key Vault (software-protected)Included in vault, no separate per-key fee for software keys$0.03 per 10,000 key operationsSame meter covers sign, verify, encrypt, decrypt
Azure Key Vault (HSM-protected)Roughly $1-5 per HSM key per month$0.03 per 10,000 (RSA-2048 class) / $0.15 per 10,000 (advanced)Managed HSM pool adds roughly $3.20/hour separately
Google Cloud KMS (software HMAC key version)~$0.06/month per active key versionBilled per cryptographic operation, symmetric-rate bucketCheapest of the three managed options
Google Cloud KMS (HSM HMAC key version)~$1.00/month per active key versionSame symmetric-rate bucketMatches AWS KMS per-key pricing closely
Google Cloud KMS (external/EKM key version)~$3.00/month per active key versionSame symmetric-rate bucketFor externally managed key material
Self-managed (environment variable or secrets manager)$0 to a few dollars/month for a secrets manager$0 (no per-call charge)You own rotation, storage, and audit logging yourself
Plain SHA-256, no key$0$0No secret exists, so there’s nothing to manage or pay for

The practical read: for most teams doing webhook or API signing at normal traffic volumes, the per-operation cost of a managed HMAC key is trivial, often a few dollars a month even at millions of calls. The bigger cost is operational, not financial: someone has to own key rotation, revocation, and secure distribution to every service that verifies a signature. That overhead simply doesn’t exist for SHA-256 checksums, because there’s no shared secret to protect in the first place.

Real-World Examples: Who Uses What, and Why

The clearest way to internalize the difference is to look at production systems that made the choice already, and see why they landed where they did.

  • Stripe webhooks: Stripe’s documentation instructs developers to compute an HMAC with SHA256 over the signed payload using the endpoint’s secret, then compare it against the signature header sent with the event. Plain SHA-256 would let anyone forge a fake “payment succeeded” event.
  • GitHub webhooks: GitHub’s webhook validation guidance centers on the X-Hub-Signature-256 header, which carries an HMAC-SHA256 signature computed with a secret you set when configuring the webhook, letting your server confirm a payload actually came from GitHub.
  • Shopify webhooks: Shopify sends a base64-encoded HMAC-SHA256 signature in the X-Shopify-Hmac-SHA256 header and instructs developers to verify it against the raw, unmodified request body before trusting the payload.
  • AWS Signature Version 4: AWS’s API request-signing scheme, used across nearly every AWS service call, derives a chain of keys and signs the canonical request with HMAC-SHA256, giving AWS proof that the request came from a holder of valid credentials, not just anyone who can guess an endpoint.
  • JWT HS256: The HS256 algorithm identifier inside a JSON Web Token specifies HMAC-SHA256 signing with a shared secret. Anyone who doesn’t hold that secret can’t mint a valid token, which is the entire security model behind symmetric-key JWTs.
  • Git object hashing: Git identifies commits, trees, and blobs by their SHA-256 (or legacy SHA-1) hash, with no secret key involved at all. This is deliberate. Git needs content-addressing and deduplication, not authentication of who created an object.
  • Software download checksums: Open-source projects publish a plain SHA-256 checksum next to a release file so users can confirm the download wasn’t corrupted or swapped in transit, assuming the checksum itself was fetched over a channel the user trusts.
  • Slack request signing: Slack signs every request it sends to a configured app with HMAC-SHA256 using a per-app signing secret, and documents comparing the computed signature against the X-Slack-Signature header before trusting an incoming event.
  • OAuth 1.0a and legacy API signing: Older OAuth 1.0a implementations used HMAC-SHA1 to sign request parameters, a scheme that predates the widespread adoption of SHA-256 and illustrates how HMAC’s security model has outlasted several generations of underlying hash functions.

Common Mistakes and Vulnerabilities

Beyond the length-extension issue already covered, a handful of implementation mistakes show up repeatedly in code review and bug bounty reports, and they’re worth naming explicitly.

Naive equality checks are the most common. Comparing an expected HMAC tag against a received one with a standard string equality operator (== in most languages) can leak timing information, because most equality checks exit as soon as they find the first mismatched byte. An attacker who can measure response time precisely enough can, in theory, guess a valid signature one byte at a time. The fix is a constant-time comparison function, such as Python’s hmac.compare_digest, Node’s crypto.timingSafeEqual, or Go’s hmac.Equal, all of which take the same amount of time regardless of where the mismatch occurs.

Verifying against a re-serialized body instead of the raw request body is another recurring bug. Webhook providers like Stripe and Shopify sign the exact raw bytes of the request. If your framework parses the JSON body first and then re-serializes it before computing the comparison hash, even a harmless difference in key ordering or whitespace will break verification, and developers sometimes “fix” this by disabling signature checks entirely, which defeats the whole point.

Skipping replay protection rounds out the list. An HMAC signature proves a message is authentic and unmodified, but it says nothing about when it was sent. Without a timestamp included in the signed payload and a freshness check on the receiving end, an attacker who captures a valid signed request can replay it later. Stripe and similar providers include a timestamp in the signed string specifically so receivers can reject stale requests.

A JWT-specific version of this problem is the algorithm-confusion attack, sometimes called the “alg: none” bug. A JWT header declares which algorithm signed the token, and some early JWT libraries trusted that header without restricting it to an allow-list, letting an attacker submit a token with the algorithm field changed to “none” and no signature at all, or worse, swap an asymmetric RS256 token to symmetric HS256 and sign it with the server’s own public key treated as an HMAC secret. The fix is straightforward but easy to skip: always specify and enforce the expected algorithm explicitly on the verifying side, and never let the token’s own header dictate which verification path runs.

Language and Library Support

Every mainstream language ships both primitives in its standard library or a near-standard crypto package, so there’s rarely a reason to hand-roll either one.

Language / ToolSHA-256HMAC-SHA256
Pythonhashlib.sha256()hmac.new(key, msg, hashlib.sha256)
Gocrypto/sha256crypto/hmac with sha256.New
Node.jscrypto.createHash('sha256')crypto.createHmac('sha256', key)
OpenSSL CLIopenssl dgst -sha256openssl dgst -sha256 -hmac key

Go’s crypto/hmac package implements HMAC per NIST FIPS 198, and its documentation notes FIPS-only constraints when the Go FIPS build mode is enabled, worth checking if your organization has compliance requirements around approved algorithms. Python’s hashlib module also exposes pbkdf2_hmac, which uses HMAC internally as its pseudorandom function for password-based key derivation, a different but related use of the same primitive.

Use-Case Recommendations

Rather than a blanket “use HMAC, it’s more secure” answer, the right choice depends entirely on whether you’re defending against tampering by an untrusted third party or just detecting accidental corruption.

  • Verifying webhook payloads from a third-party service: Use HMAC-SHA256 with the secret the provider gives you. This is non-negotiable. Plain SHA-256 provides no protection against a forged event.
  • Signing API requests between your own services: Use HMAC-SHA256, following the AWS SigV4 pattern of deriving scoped keys rather than reusing one long-lived secret everywhere.
  • Verifying a downloaded file wasn’t corrupted: Plain SHA-256 is fine, since you’re checking integrity against accidental damage, not defending against an active attacker who controls the checksum too.
  • Content-addressed storage or deduplication: Use plain SHA-256 (or a purpose-built hash like BLAKE3). There’s no authentication requirement, only a need for a consistent, collision-resistant identifier.
  • Symmetric JWT signing (HS256): HMAC-SHA256 is what the algorithm is, by definition. If you need per-user or asymmetric verification instead, look at RS256 or ES256, not plain SHA-256.
  • Password storage: Neither one. Use a purpose-built password hash like Argon2 or bcrypt, which are deliberately slow and salted. SHA-256 and HMAC-SHA256 are both far too fast for that job and will fall to GPU cracking rigs quickly.
  • Git commit or object identity: Plain SHA-256, matching git’s own design, since the goal is content addressing, not sender authentication.

Migration Guide: Moving From Raw SHA-256 to HMAC-SHA256

If you inherited a system that authenticates requests with something like SHA256(secret + payload) instead of proper HMAC, here’s a practical path to fix it without breaking existing integrations overnight.

  1. Audit every place in the codebase that concatenates a secret with a message before hashing it. Search for patterns like sha256(secret + data) or SHA256(key + payload) across your services.
  2. Generate a new, sufficiently long secret (32 bytes minimum) for each system that needs one, rather than reusing an old value that may have been exposed through the vulnerable construction.
  3. Implement HMAC-SHA256 signing on the sending side using your language’s standard library HMAC function, never a hand-rolled concatenation.
  4. Implement verification on the receiving side using a constant-time comparison function specifically, not a standard equality operator, to avoid introducing a timing side channel while you’re fixing the original bug.
  5. Run both the old and new verification schemes in parallel for a transition window, accepting either signature, so existing clients don’t break the moment you deploy.
  6. Add a timestamp to the signed payload if one doesn’t already exist, and reject requests outside a reasonable time window, closing the replay gap at the same time.
  7. Rotate all secrets used by the old scheme once every client has migrated, since those secrets were exposed to a construction with a known theoretical weakness even if it was never actively exploited against you.
  8. Remove the legacy verification path once telemetry shows zero requests using it, and document the change so the mistake doesn’t get reintroduced by a future contributor.

Key Length and Rotation Best Practices

HMAC’s security depends on the secret key staying secret, so how you generate, size, and rotate that key matters as much as choosing HMAC over plain SHA-256 in the first place. RFC 2104 recommends a key length at least equal to the output size of the underlying hash, which for HMAC-SHA256 means 32 bytes (256 bits). Shorter keys don’t break the construction outright, but they shrink the search space an attacker would need to brute-force, and there’s no good reason to cut corners here since generating a 32-byte random key costs nothing.

Keys longer than the hash’s block size (512 bits for SHA-256) get hashed down before use inside the HMAC construction itself, so padding a key out to some enormous length doesn’t buy extra security once you’re past the block size. The practical sweet spot is generating exactly 32 random bytes with a cryptographically secure random number generator, such as Python’s secrets.token_bytes(32) or Node’s crypto.randomBytes(32), and treating that value with the same handling rules as a database password or API key.

Rotation is where most teams fall short in practice. A webhook secret that’s never rotated is a secret that, once leaked through a log file, a misconfigured error tracker, or a compromised CI pipeline, stays exploitable indefinitely. The cleanest rotation pattern accepts two valid secrets simultaneously during a transition window: generate a new secret, configure the receiving service to accept signatures computed with either the old or new key, update the sending side to use the new key, confirm traffic has shifted over via logging, then retire the old key. This mirrors exactly the migration pattern already covered above for moving off a broken length-extension-vulnerable scheme, and it’s worth building as a repeatable runbook rather than a one-time fire drill.

Pros and Cons

SHA-256 (Unkeyed)

  • Pro: Zero secret management overhead, since there’s no key to store or rotate.
  • Pro: Slightly faster than HMAC-SHA256, roughly half the cycles per byte in most benchmarks.
  • Pro: Perfect for content-addressing, deduplication, and checksums where trust isn’t the question.
  • Con: Provides no authentication whatsoever. Anyone can produce a valid hash for any content.
  • Con: Vulnerable to length-extension attacks if misused as a homemade MAC via key concatenation.
  • Con: Frequently misused by developers who don’t realize it isn’t an authentication mechanism.

HMAC-SHA256

  • Pro: Provides real authentication, proving a message came from a holder of the shared secret.
  • Pro: Immune to length-extension attacks by construction, unlike naive key-prepend schemes.
  • Pro: Universally supported, standardized in RFC 2104, and used by every major webhook and API-signing system.
  • Con: Requires generating, storing, distributing, and eventually rotating a secret key.
  • Con: Roughly 1.8x-2.2x the CPU cost of plain SHA-256, due to the two-pass construction.
  • Con: Introduces new failure modes if implemented carelessly, like non-constant-time comparisons or raw-body mismatches.

The Verdict

SHA-256 and HMAC-SHA256 aren’t rivals competing for the same job, so “which one wins” is the wrong frame. The right question is which job you’re actually solving. If you need to detect accidental corruption in a file, a git object, or a piece of content with no adversary trying to forge it, plain SHA-256 does that job well, for free, with no key management burden and a small speed advantage. The moment you need to prove a message came from a specific party holding a secret, whether that’s a webhook from Stripe, a signed AWS API call, or a JWT your own backend issued, HMAC-SHA256 is the only correct choice, and the roughly 2x CPU overhead and the handful of dollars a month in managed-key costs are not a real obstacle for the vast majority of systems.

The data backs this up cleanly: HMAC-SHA256’s overhead, whether measured in cycles per byte or in AWS KMS’s $0.03-per-10,000-requests pricing, is trivial next to the cost of a forged webhook or a spoofed API call slipping through unauthenticated. The mistake worth eliminating from any codebase isn’t choosing the “wrong” one of these two, it’s using plain SHA-256 where authentication was actually required, or building a homemade key-concatenation scheme instead of reaching for the HMAC implementation that’s already sitting in your standard library.

Frequently Asked Questions

Is HMAC-SHA256 more secure than SHA-256?
They solve different problems, so “more secure” depends on what you’re protecting against. For detecting accidental corruption, plain SHA-256 is sufficient. For proving a message came from a trusted sender, only HMAC-SHA256 provides that guarantee, because plain SHA-256 has no concept of a secret.

Can I just do SHA256(secret + message) instead of using HMAC?
No. That construction is vulnerable to length-extension attacks, where an attacker can extend a message and compute a valid hash for the extended version without knowing the secret. HMAC’s inner/outer double-hash design exists specifically to close this gap.

How much slower is HMAC-SHA256 than plain SHA-256?
Roughly 1.8x to 2.2x the cycles per byte in most benchmarks, since HMAC runs the hash function about twice per message. For nearly all applications this difference is invisible. It only matters at extreme signature-verification volumes.

Do Stripe, GitHub, and Shopify all use HMAC-SHA256 for webhooks?
Yes. All three sign webhook payloads with HMAC-SHA256 using a secret provided when you configure the webhook, and their documentation instructs developers to verify signatures using a constant-time comparison against the raw request body.

What does the JWT algorithm HS256 actually mean?
HS256 is HMAC-SHA256. It’s a symmetric signing scheme, meaning the same secret both signs and verifies the token, as opposed to RS256 or ES256, which use asymmetric key pairs.

Should I use SHA-256 or HMAC-SHA256 for password storage?
Neither. Both are far too fast for password hashing and are vulnerable to brute-force and GPU cracking at scale. Use a dedicated password-hashing algorithm like Argon2 or bcrypt instead, which are deliberately slow and include salting.

Why does HMAC cost money on AWS but SHA-256 doesn’t?
SHA-256 needs no secret, so there’s nothing for a cloud key management service to store or protect. HMAC requires a key, and AWS KMS, Azure Key Vault, and Google Cloud KMS all charge for storing and using that key, typically around $1/month per key plus a small per-request fee.

Is it safe to compare HMAC signatures with a normal equality operator?
No. Standard equality checks can exit early on the first mismatched byte, creating a timing side channel an attacker could exploit. Use a constant-time comparison function, such as Python’s hmac.compare_digest or Node’s crypto.timingSafeEqual.

How long should an HMAC-SHA256 secret key be?
RFC 2104 recommends a key at least as long as the hash output, so 32 bytes (256 bits) for HMAC-SHA256. Generate it with a cryptographically secure random number generator rather than a password or passphrase, and there’s no security benefit to making it longer than the 64-byte block size of SHA-256.

Can plain SHA-256 be used for password hashing if I add a salt?
Adding a salt to SHA-256 stops precomputed rainbow-table attacks, but SHA-256 is still fast enough that a GPU or ASIC can brute-force billions of guesses per second against it. Dedicated password hashes like Argon2 or bcrypt are deliberately slow and memory-hard, which is what actually raises the cost of a brute-force attack, salt or no salt.