Systems

JWT: What Every Engineer Should Know

JSON Web Tokens are everywhere, yet most engineers treat them as black boxes. Here's the mental model that changes how you debug auth.

Rickvian Aldi·Software engineer·April 10, 2026·7 min read

You've pasted it into a form before. Copied it out of an Authorization header, stared at three dots separating what looks like Base64 noise, and wondered what any of it actually means. JWT - JSON Web Token - is one of those technologies you absorb through osmosis rather than deliberate study. Then, one day, you get a cryptic 401 Unauthorized in production and realize the osmosis wasn't enough.

This essay is the deliberate study version. We'll cover the structure, the claims that matter, the expiry trap engineers fall into, and the security property JWTs actually give you versus the ones you might be assuming.

The Three-Segment Structure

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three segments separated by dots. Each segment is Base64url-encoded - like regular Base64 but with + replaced by - and / by _, so the result is URL-safe. You can decode each segment independently without any secret or key.

Segment one: the header. Decode it and you get JSON like:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg names the signing algorithm. HS256 means HMAC-SHA256, a symmetric algorithm where the same secret signs and verifies. RS256 means RSA with SHA-256, an asymmetric algorithm where a private key signs and a public key verifies. This distinction matters more than most engineers realize - we'll get to it.

Segment two: the payload. Also JSON, containing claims - assertions about the subject:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

Claims are just key-value pairs. Some are registered (standardized by the RFC, like sub, iat, exp). Others are public (widely used by convention, like email, roles). The rest are private (application-specific, like org_id, plan_tier).

Segment three: the signature. The cryptographic proof that the header and payload haven't been tampered with. Computed over base64url(header) + "." + base64url(payload) using the algorithm named in the header. This is the only part you cannot decode without the secret or public key.

The Claims That Actually Matter

Most JWTs carry a handful of registered claims. Know these by heart:

sub (Subject): Who the token is about - typically a user ID. Opaque to the client, meaningful to your backend.

iss (Issuer): Who issued the token. Useful when your system accepts tokens from multiple identity providers. Always validate this if you do.

aud (Audience): Who the token is intended for. If your API has an aud of api.example.com, reject tokens with a different audience even if the signature is valid. Many teams skip audience validation and regret it.

iat (Issued At): Unix timestamp (seconds since epoch) when the token was created. Useful for detecting tokens that are suspiciously old even if not yet expired.

exp (Expiry): The timestamp after which the token must be rejected. This is the claim responsible for the most 401 errors on a Monday morning.

nbf (Not Before): The timestamp before which the token must not be accepted. Less commonly used, but available.

The Expiry Trap

exp is a Unix timestamp in seconds, not milliseconds. This traps developers who compare it against Date.now(), which returns milliseconds:

// Wrong: Date.now() returns milliseconds
if (payload.exp < Date.now()) { /* always expires immediately */ }
 
// Correct: compare seconds to seconds
if (payload.exp < Math.floor(Date.now() / 1000)) { /* correct */ }

The second trap is clock skew. If your token-issuing server and your verifying server have clocks that differ by even a few seconds, a just-issued token can appear expired. Production systems address this with a small leeway - typically 30–60 seconds - added to exp before rejecting. Most JWT libraries support configuring leeway directly.

The third trap is timezone confusion. exp is always UTC. It has no timezone. Tools that display it in local time can make a token that expires at midnight UTC appear to expire at midnight local time, throwing off your debugging.

What the Signature Actually Guarantees

This is where engineers carry incorrect mental models the longest.

The signature guarantees integrity: if someone modifies the payload after issuance, the signature check fails. You'll know the token was tampered with.

The signature does not guarantee confidentiality: the payload is Base64-encoded, not encrypted. Anyone who holds the token can read every claim without the signing key. Never put sensitive data in a JWT payload - no passwords, no PII you'd rather keep private, no security-sensitive flags.

For confidentiality, you want JWE (JSON Web Encryption), a different standard that actually encrypts the payload. JWE is less common in practice precisely because most use cases only need integrity, not secrecy.

Symmetric vs. Asymmetric: Why It Matters in Microservices

With HS256 (symmetric), both the issuer and every verifier share the same secret. This works fine for a monolith. In a microservices architecture it forces every service that validates JWTs to hold the signing secret - which means every one of those services is now a high-value target. Compromise any one of them and an attacker can mint arbitrary tokens.

With RS256 (asymmetric), the auth service holds the private key and signs tokens. Every other service holds only the public key, which it uses to verify. The public key is, as the name implies, public - you can distribute it freely. An attacker who compromises a downstream service gets nothing useful for forging tokens.

If you're building a new system with more than two services, start with RS256. The operational complexity is modest; the security posture improvement is significant.

The Stateless Tradeoff

JWTs are often described as "stateless authentication." The auth server doesn't need to store session records - all the information needed to validate a request travels with the token. This is genuinely useful for horizontal scaling: any instance of your service can validate any token without a database lookup.

The cost is that you cannot revoke a JWT before it expires. Revocation requires statefulness. The common mitigations:

Short expiry + refresh tokens. Issue access tokens that expire in 15 minutes. Issue a long-lived refresh token separately and store it server-side. When the access token expires, the client exchanges the refresh token for a new access token. Revocation means invalidating the refresh token.

Token blocklist. Store revoked JTIs (jti - the JWT ID claim, a unique identifier for the token) in Redis. Check every incoming token against the blocklist. This adds a cache lookup per request but preserves the scalability benefit.

Leaky bucket. For most applications, short expiry is enough. If a user changes their password, revoke their refresh tokens; their access tokens will naturally expire within minutes. Perfect revocation is less important than it sounds for most use cases.

Reading JWTs in the Wild

Knowing the structure lets you debug auth failures faster. When a request gets a 401, before reaching for logs:

  1. Copy the token from the request header.
  2. Paste it into a decoder (like the one on this site - it runs entirely in your browser).
  3. Check exp against the current UTC timestamp. Expired? That's your answer.
  4. Check iss and aud. Does your service accept this issuer? Is the audience correct?
  5. Check the claims your service cares about. Is the sub in your user table? Is the role claim present?

Most 401 Unauthorized errors fall into one of these buckets. You don't need to verify the signature to diagnose expiry or claim issues - you just need to read the payload.

A Note on Security

None of the above replaces proper security review. JWTs are a tool with a specific threat model: they protect against payload tampering in transit. They don't protect against token theft (use HTTPS, secure cookie flags, short expiry), implementation bugs (use a vetted library, not a handrolled one), or weak secrets (for HS256, your secret should be at least 256 bits of entropy - a random string from a CSPRNG, not a password).

The alg: "none" attack is worth knowing: early JWT implementations accepted tokens where the header set alg to none, bypassing signature verification entirely. Modern libraries reject this by default, but if you're ever auditing a custom implementation, look for it.

Understanding the format won't make you immune to auth bugs. But it will make you faster at diagnosing them and more deliberate about the tradeoffs your auth architecture is making.

Related essays

Systems

Five Message Broker Patterns

I kept dropping names like Saga, CQRS, and Outbox in design reviews without being fully honest about which one solved what. A ByteByteGo infographic pushed me to stop faking it and draw each one from memory. These are the diagrams - and the use cases - that finally made them stick. testing

Apr 20, 2026·13 min read
Systems

Why Engineers Are Obsessed With P99

If you only watch the average, you are watching the wrong number. P99 is where the money leaks, where the outages start, and where your users quietly decide to leave. testing

Apr 20, 2026·12 min read

Get essays in your inbox

Practical deep-dives on software craft, career leverage, and building things that matter. No noise.