Three segments
A JWT is three encoded segments joined by dots: a header, a payload, and a signature.
The header is a tiny object naming the signing algorithm. The payload carries the claims. Some have standard meanings: who the user is, when the token expires, when it was issued, who minted it and who it is for. Then whatever you add yourself, like roles or tenant identifiers. The signature covers the first two segments, so changing either one invalidates it.
Learn the single most important fact about the format. That encoding is not encryption.
Anyone holding one of your tokens can decode the payload in one line of code, or by pasting it into a website. The signature stops tampering, and it does not stop reading. Email addresses in there are already a grey area, and anything genuinely sensitive is an incident waiting on one leaked log line.
Size, and staleness
Watch the size, which matters more than people expect, because this thing rides on every single request.
A minimal token runs around 200 bytes. Stuff a full profile and permission list into it and you are at 2 kilobytes, heading for the 8 kilobyte header limits your proxies and CDNs enforce by default. Group membership claims are the classic offender.
Your claims freeze at the moment of signing. The token says admin until it expires, even though you demoted that user an hour ago.
Staleness as the shadow of the revocation problem in the third lesson. It is also the argument for keeping claims minimal: an identity and a coarse role, not a snapshot of your permissions table.
Worked example
During a security review at a lending startup, a contractor named Tomasz proxies the Android app through Burp Suite and copies the session token, a JWT. Decoding the middle segment takes one line of Python. Inside he finds the user's email, phone number, credit tier, and a boolean claim named bypass_kyc left over from an internal testing build. Nothing was broken in the cryptographic sense: the signature was valid and the algorithm strong. But the payload had become a dumping ground, 1.9 KB attached to each of the roughly 40 API calls the app made per session, leaking data the backend never meant clients to see. The fix took one sprint: claims cut down to sub, exp, iss, aud, and a role, everything else moved behind a /me endpoint, and the token shrank to 310 bytes.