Cassandra's per-node throughput is often gated by GC pauses. JVM tuning is no longer arcane: with G1 GC and modern releases, the defaults are usually fine — but heap sizing, GC choice, and a few flags still matter.
Heap sizing — don't oversize
8GB-16GB heap is right for most nodes. Larger heaps make pauses worse, not better. Off-heap caches (key cache, bloom filters) live outside this; they consume the remaining RAM. Cassandra 4+ defaults to G1; older 3.x used CMS.
G1 GC — default, mostly leave alone
Targets pause-time goals (<200ms). Right for 8-32GB heaps. Key flag: -XX:MaxGCPauseMillis=200. Don't lower under 100ms — G1 will fragment heap badly.
ZGC — for very large heaps
Sub-10ms pauses regardless of heap size. Pays ~10% throughput cost. Right for 64GB+ heaps. Cassandra 4.1+ tested. Enable with -XX:+UseZGC. Don't use on 8GB heaps — wasted complexity.
Off-heap memory
Cassandra uses ~50% of system RAM off-heap for memtable_offheap, bloom filters, key cache. So 64GB node: ~12GB heap + ~32GB off-heap + ~20GB OS file cache. Don't starve the OS file cache — it's how most read hits stay fast.
What to monitor
GC pause time (p99 <100ms is good), heap occupancy after old-gen GC (should drop to <50%), allocation rate. nodetool gcstats + jstat. If pauses creep above 500ms regularly: investigate before adding nodes.