ML-DSA signature and key sizes
ML-DSA-44: 2,420-byte signature, 1,312-byte public key.
ML-DSA-65: 3,309-byte signature, 1,952-byte public key.
ML-DSA-87: 4,627-byte signature, 2,592-byte public key.
An ML-DSA-65 signature is 52× the size of an Ed25519 signature (64 bytes), and 4,412 bytes once Base64-encoded.
Raw sizes (FIPS 204)
ML-DSA (Module-Lattice-Based Digital Signature Algorithm) is standardised in FIPS 204. All three parameter sets have fixed-size signatures, so these are exact values, not averages.
| Parameter set | Security | Public key | Private key | Signature | vs. Ed25519 sig |
|---|---|---|---|---|---|
| ML-DSA-44 | NIST level 2 | 1,312 | 2,560 | 2,420 | 38× |
| ML-DSA-65 | NIST level 3 | 1,952 | 4,032 | 3,309 | 52× |
| ML-DSA-87 | NIST level 5 | 2,592 | 4,896 | 4,627 | 72× |
| Ed25519 | classical | 32 | 64 | 64 | 1× |
| ECDSA P-256 | classical | 64 | 32 | 64 (70–72 DER) | 1× |
| RSA-2048 | classical | 256 | 1,190 | 256 | 4× |
Bytes. ECDSA public key shown as raw x‖y; the private-key size for ML-DSA is the expanded FIPS 204 encoding (implementations may store a 32-byte seed instead).
Encoded signature sizes
Signatures are rarely stored raw. Base64 adds a third, hex doubles them, and a JWT adds its header and claims on top.
| Parameter set | Raw | Hex | Base64 | Base64url | DER BIT STRING | JWT (279 B claims) |
|---|---|---|---|---|---|---|
| ML-DSA-44 | 2,420 | 4,840 | 3,228 | 3,227 | 2,425 | 3,507 |
| ML-DSA-65 | 3,309 | 6,618 | 4,412 | 4,412 | 3,314 | 4,692 |
| ML-DSA-87 | 4,627 | 9,254 | 6,172 | 6,170 | 4,632 | 6,450 |
What ML-DSA breaks
The same artifacts checked against limits commonly found in real stacks. Hard limits like the cookie cap and the B-tree tuple can't be raised by configuration.
| Limit | Carried as | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|---|
| QUIC datagram | 1,200 B, raw | +1,220 | +2,109 | +3,427 |
VARCHAR(255) | Base64 | +2,973 | +4,157 | +5,917 |
VARCHAR(1024) | Base64 | +2,204 | +3,388 | +5,148 |
| PostgreSQL B-tree index | 2,704 B, Base64 | +524 | +1,708 | +3,468 |
| Browser cookie | 4,096 B, as JWT | fits | +596 | +2,354 |
| mbedTLS record buffer | 16,384 B, 3-cert chain | fits | +184 | +6,058 |
Overflow in bytes. Cookie and header JWTs assume 279 B of header and claims. TLS assumes three certificates with minimal X.509 metadata; real certificates are larger.
How big should the database column be?
Sized with Tokenkeep's column policy: the Base64 length plus 25% headroom, rounded up to a power of two. If you store raw bytes, use BYTEA / VARBINARY and skip Base64 entirely. Don't put a B-tree index on the signature itself: index a SHA-256 digest of it instead.
| Parameter set | Base64 signature | Column for signature | Base64 public key | Column for public key |
|---|---|---|---|---|
| ML-DSA-44 | 3,228 | VARCHAR(4096) | 1,752 | VARCHAR(4096) |
| ML-DSA-65 | 4,412 | VARCHAR(8192) | 2,604 | VARCHAR(4096) |
| ML-DSA-87 | 6,172 | VARCHAR(8192) | 3,456 | VARCHAR(8192) |
ML-DSA vs. Dilithium sizes
ML-DSA is the standardised form of CRYSTALS-Dilithium. Public keys are unchanged, but the final standard changed the signature encoding: round-3 Dilithium3 signatures were 3,293 bytes and Dilithium5 4,595 bytes, against 3,309 and 4,627 for ML-DSA-65 and ML-DSA-87. If you sized columns or buffers from older Dilithium documentation, check them again.
Find every limit these sizes break in your stack
Paste a migration, an nginx.conf or an mbedTLS header into the checker, or ask us for a free scan of the whole repository.