JWT in APIs: What It Does Well and Where It Falls Short

by Eric Hanson, Backend Developer at Clean Systems Consulting

What JWT actually is

A JSON Web Token (defined in RFC 7519) is a base64url-encoded JSON payload, signed by a key you hold. The signature lets any party with the corresponding verification key confirm that the claims in the token were issued by you and have not been tampered with — without querying a database.

A minimal JWT payload:

{
  "sub": "user_01HZQK7P3WVXBN",
  "roles": ["orders:read", "orders:write"],
  "iat": 1745654400,
  "exp": 1745658000
}

sub is the subject (user identifier). iat is issued-at. exp is expiry. The token is signed with either an HMAC-SHA256 secret (symmetric — both issuer and verifier share the secret) or an RSA/EC key pair (asymmetric — the issuer signs with the private key, verifiers use the public key).

The asymmetric approach (RS256, ES256) is strongly preferred for any multi-service architecture. It allows your resource servers to verify tokens without having access to the signing secret, which they would need to forge tokens if it were leaked.

What JWTs do well

Stateless verification. Your API gateway or service can verify a JWT with a single cryptographic operation. No database lookup. No network call to an auth service. At high request volumes (thousands per second per node), this matters — the alternative is a session lookup that adds latency and creates a bottleneck on your auth store.

Embedded claims. The token carries the information the service needs to make authorization decisions: user ID, roles, scopes, tenant ID. The service does not need to look up permissions; they are in the token.

Cross-service trust. In a microservice architecture, service A can issue a JWT that service B accepts, even if they are completely independent systems, as long as both know the issuer's public key. This is clean and scalable.

Where JWTs fall short

Revocation is the central problem. A valid JWT is valid until it expires — full stop. If a user logs out, changes their password, or has their account compromised, you cannot invalidate an outstanding JWT. It will continue to be accepted by any verifier until exp.

The workaround is a token blocklist: a fast store (Redis is the standard choice) that tracks revoked token IDs (jti claim). Every verification step checks the blocklist. This works but reintroduces the network roundtrip you were trying to avoid, and your blocklist becomes a critical-path dependency.

The practical resolution: use short expiry (15 minutes is common for access tokens) and issue long-lived refresh tokens separately. The refresh token is stored server-side and can be revoked. The access token can expire frequently enough that the revocation window is acceptable.

The payload is not encrypted by default. A JWT is signed, not encrypted. Base64url is not encryption — anyone with the token can decode the payload and read the claims. Do not put sensitive data in a JWT unless you are using JWE (JSON Web Encryption, a different spec) to encrypt it.

This matters for API keys passed between services and for tokens stored in browser localStorage. The claims are visible to anyone who can read the token.

Algorithm confusion attacks. The JWT header specifies the algorithm used to sign the token. Early library implementations allowed an attacker to specify "alg": "none" in a crafted token and bypass signature verification entirely. Some libraries also accepted "alg": "HS256" on tokens signed with an RS256 key pair, treating the public key as the HMAC secret.

The fix: always specify the accepted algorithm explicitly in your verification code. Never accept none. Never derive the algorithm from the token header alone.

# Wrong
jwt.decode(token, public_key)  # trusts alg from header

# Right
jwt.decode(token, public_key, algorithms=["RS256"])  # explicitly requires RS256

Use python-jose or PyJWT >= 2.0 which default to secure behavior, but verify your library's defaults either way.

Token size. JWTs with many claims grow large. A token with dozens of role and scope claims can easily exceed 1KB. Multiplied across every request, this adds up in network overhead and header parsing time. Keep claims minimal — the token should carry what the service needs, not everything the user has ever been granted.

The libraries worth knowing

  • Java: java-jwt (Auth0) or jjwt (JJWT project). Both support RS256/ES256 and have sane defaults post-2020.
  • Node.js: jsonwebtoken for basic use; jose (by Panva) for more complete JOSE spec support including JWE.
  • Python: PyJWT >= 2.0 with explicit algorithm specification.
  • Go: golang-jwt/jwt (the community-maintained fork after the original was abandoned).

The right mental model

A JWT is a signed, time-limited statement: "at this time, this issuer asserted these claims about this subject." It is not a session. It is not a capability. It is a claim that must be verified structurally (valid signature), temporally (not expired), and contextually (issuer matches what you expect, audience matches your service).

Treat it that way and it is a solid building block. Treat it as a magic ticket and the gaps will bite you.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Why Office-Only Policies Don’t Solve Security or Productivity Problems

“We need everyone back in the office for security and productivity.” It sounds responsible—until you look at what actually improves those things.

Read more

Why Developers Need Time to Refactor Code

Refactoring often feels like unproductive work. But skipping it is like ignoring weeds in a garden—they’ll choke everything else eventually.

Read more

Why Backend Systems Fail at Scale

“It worked perfectly… until we got users.” Scale doesn’t break systems — it reveals what was already fragile.

Read more

Why an Ideal Engineering Team Needs More Than Just Full-Stack Developers

Hiring a few “full-stack developers” sounds like the efficient choice. But relying on them alone often creates hidden gaps that slow everything down.

Read more