Skip to content
ecdsa.com

AES-GCM vs ChaCha20-Poly1305: which AEAD to use

Both are standard 128-bit-tag AEADs with 96-bit nonces. AES-GCM wins on CPUs with AES instructions and in FIPS environments; ChaCha20-Poly1305 wins everywhere else.

AES-GCM vs ChaCha20-Poly1305 is the choice between the two authenticated encryption algorithms that carry almost all encrypted traffic: TLS 1.3, QUIC, SSH, WireGuard, age and most application-level encryption. Both are secure when used correctly, both fail badly when a nonce repeats, and their speed depends heavily on the hardware.

At a glance

AES-GCMChaCha20-Poly1305
SpecificationNIST SP 800-38D (2007)RFC 8439 (2018, replacing RFC 7539)
Key sizes128, 192, 256 bits256 bits
Nonce96 bits recommended96 bits (192 bits in XChaCha20)
Tag128 bits (shorter allowed, not recommended)128 bits
Max data per messageabout 64 GiB (2³² − 2 blocks)256 GiB (2³² 64-byte blocks)
Random-nonce budget per key2³² messages (SP 800-38D)Same order with 96-bit nonces; practically unlimited with XChaCha20
Nonce reuseLeaks XOR of plaintexts and the GHASH key, enabling forgeriesLeaks XOR of plaintexts and enables forgeries
Throughput, 16 KiB, OpenSSL 3.6.510.4 GB/s (AES-128), 8.8 GB/s (AES-256)2.1 GB/s
Throughput, 16-byte messages0.14 GB/s (AES-256)0.52 GB/s
Constant time without hardwareHard (table-based AES leaks through caches)Natural (add, rotate, XOR)
FIPS 140-3 approvedYesNo
TLS 1.3TLS_AES_128_GCM_SHA256 (MUST), TLS_AES_256_GCM_SHA384 (SHOULD)TLS_CHACHA20_POLY1305_SHA256 (SHOULD)
Other usesJWE A128GCM and A256GCM, IPsec, SSH aes256-gcm@openssh.com, disk and cloud encryptionWireGuard, SSH chacha20-poly1305@openssh.com, age, libsodium

Throughput measured with openssl speed -evp on one core of an Apple M4 Max, which has AES and polynomial-multiply instructions.

Performance depends on the CPU

AES-GCM is built for hardware: AES rounds run as single instructions (AES-NI on x86, the ARMv8 cryptography extensions) and GHASH uses carry-less multiplication. With both available it reaches about 10 GB/s per core here, about five times ChaCha20-Poly1305. Without them, AES must use lookup tables, which are slow and leak key bits through cache timing, or bitsliced code, which is constant time but slow. ChaCha20 needs only 32-bit additions, rotations and XORs, so it runs fast and in constant time on any CPU, including low-end phones and microcontrollers.

Small messages narrow or reverse the gap: with 16-byte inputs, ChaCha20-Poly1305 reached 0.52 GB/s against 0.14 GB/s for AES-256-GCM, because GCM's setup cost dominates. For typical TLS records (up to 16 KiB) on servers, AES-GCM is faster; for many tiny packets the difference is small either way.

Nonces and limits

Both are counter-mode ciphers with one-time authenticators, so the (key, nonce) pair must never repeat. A repeat reveals the XOR of the two plaintexts; with GCM it also lets an attacker recover the authentication subkey and forge messages under that key, and with Poly1305 it exposes the one-time key and allows forgery for that nonce. NIST SP 800-38D limits randomly generated 96-bit IVs to 2³² invocations per key to keep the collision probability negligible; TLS sidesteps the issue by deriving nonces from record sequence numbers and rekeying.

If your design generates random nonces for many messages under one key, use a construction with more nonce space or misuse resistance:

  • XChaCha20-Poly1305 (192-bit nonces, described in an IRTF Internet-Draft and implemented by libsodium and others), where random nonces are safe for practically unlimited messages;
  • AES-GCM-SIV (RFC 8452), where nonce reuse only reveals whether two messages were identical;
  • or key derivation per message or per session, as TLS, age and HPKE do.

Neither algorithm commits to its key: a ciphertext can be crafted to decrypt validly under two different keys, which matters for multi-recipient or password-based designs (the "invisible salamanders" problem). Add a key commitment if attackers can choose keys.

Node.js 26: the same AEAD interface for both algorithms
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";

const plaintext = Buffer.from("card ending 4242, amount 19.99");
const aad = Buffer.from("order=7781");

for (const algorithm of ["aes-256-gcm", "chacha20-poly1305"]) {
  const key = randomBytes(32);
  const nonce = randomBytes(12); // 96-bit nonce for both; never reuse with the same key
  const cipher = createCipheriv(algorithm, key, nonce, { authTagLength: 16 });
  cipher.setAAD(aad);
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const tag = cipher.getAuthTag();

  const decipher = createDecipheriv(algorithm, key, nonce, { authTagLength: 16 });
  decipher.setAAD(aad);
  decipher.setAuthTag(tag);
  const roundTrip = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

  console.log(algorithm.padEnd(18), "ciphertext", ciphertext.length, "bytes + tag", tag.length, "bytes, decrypts:", roundTrip.equals(plaintext));
}
Output
aes-256-gcm        ciphertext 30 bytes + tag 16 bytes, decrypts: true
chacha20-poly1305  ciphertext 30 bytes + tag 16 bytes, decrypts: true

How TLS picks between them

TLS 1.3 requires every implementation to support TLS_AES_128_GCM_SHA256 and recommends AES-256-GCM and ChaCha20-Poly1305. Clients without AES hardware list ChaCha20 first; servers that honor that preference serve each client the faster cipher. OpenSSL implements this with the PrioritizeChaCha option (SSL_OP_PRIORITIZE_CHACHA), which prefers ChaCha20 only when the client listed it first, and BoringSSL uses equal-preference groups for the same purpose. The TLS configuration generator produces server settings that keep both suites.

When to choose which

  • TLS and QUIC servers: enable both; prefer AES-GCM but let clients that put ChaCha20 first get it.
  • FIPS 140-3 environments: AES-GCM; ChaCha20-Poly1305 is not an approved algorithm.
  • Mobile, IoT and microcontrollers without AES acceleration: ChaCha20-Poly1305.
  • Application-level encryption with random nonces and long-lived keys: XChaCha20-Poly1305 or AES-GCM-SIV.
  • JWE: A256GCM (or A128GCM); ChaCha20 is not in the core JWA registry.
  • Very large single messages (above 64 GiB): split into chunks with per-chunk nonces (as age does with 64 KiB ChaCha20-Poly1305 chunks); do not rely on per-message limits.

Migration notes

Switching AEADs changes the ciphertext format but nothing else in a well-designed system.

  1. Version your format: store an algorithm identifier and key identifier with each ciphertext so old data stays readable.
  2. Keep the key separate per algorithm: derive distinct keys (for example with HKDF and different info strings) rather than reusing one key for both.
  3. Re-check nonce generation: counters must be persisted across restarts; random nonces must stay within the limits above or move to XChaCha20.
  4. Re-encrypt lazily: decrypt with the old algorithm and write with the new one when records are touched, unless a compliance deadline requires a bulk migration.
  5. Benchmark on the real hardware: the speed ranking flips between server CPUs and small devices.

References

All comparisons