Skip to main content

What to Fix First in a Degrading Cassandra Cluster

You're staring at Datadog. The p99 read latency graph has gone from a gentle 10ms to a jagged 200ms. Your phone buzzes: customers are seeing timeouts. Your primary instinct? Tune the heap, or add more nodes. Stop. In a Cassandra cluster that's degrading, the faulty fix can cascade into a full outage. I've seen units spend days chasing GC pauses when the real culprit was a one-off node with a failed disk holding up the entire ring. So what do you fix opening? The answer isn't sexy. It's methodical, and it starts with ruling out the obvious. According to practitioners we interviewed, the trade-off is rarely about talent — it is about handoffs, and however confident you feel after the opening pass, the pitfall shows up when someone else repeats your shortcut without the same context.

You're staring at Datadog. The p99 read latency graph has gone from a gentle 10ms to a jagged 200ms. Your phone buzzes: customers are seeing timeouts. Your primary instinct? Tune the heap, or add more nodes. Stop. In a Cassandra cluster that's degrading, the faulty fix can cascade into a full outage. I've seen units spend days chasing GC pauses when the real culprit was a one-off node with a failed disk holding up the entire ring. So what do you fix opening? The answer isn't sexy. It's methodical, and it starts with ruling out the obvious.

According to practitioners we interviewed, the trade-off is rarely about talent — it is about handoffs, and however confident you feel after the opening pass, the pitfall shows up when someone else repeats your shortcut without the same context.

Who This Triage Is For—And What Happens When You Skip It

Signs of degradation: latency spikes, repair failures, hinted handoff pile-ups

You are the person who gets paged at 2 AM because a dashboard turned red. Ops engineers. SREs. The ones holding the keys when a Cassandra cluster starts breathing hard. This triage is for you—not for architects designing greenfield systems or devs running a three-node test cluster. The signs are specific: p99 latencies that doubled overnight, repair jobs that hang for hours, or a hinted handoff backlog that refuses to drain. I have watched crews ignore a lone measured node because “the rest of the ring looks fine.” That is how a crack becomes a canyon. The dead node starts rejecting connections, coordinators retry, retries queue up, and suddenly the whole cluster is doing a measured-motion collapse. — that’s the real cost of skipping the primary symptom.

That one choice reshapes the rest of the workflow quickly.

Why random tuning is worse than doing nothing

The catch is that most crews don’t sit still—they act. They bump `concurrent_reads`, double the heap, or flip `hinted_handoff_throttle` without checking what’s actually degraded. I fixed a cluster once where someone had raised `memtable_flush_writers` to 16, thinking it would speed things up. It didn’t. It saturated the disk queue, triggered write timeouts, and turned a solo-node issue into a five-node recovery. faulty sequence. That hurts more than doing nothing, because now you have no baseline to phase back to. You trade a symptom for a worse one—latency drops but compaction storms spike. The consequence is a recovery that takes twice as long, because you are untangling your own tweaks before you can touch the real snag.

When units treat this step as optional, the rework loop usually starts within one sprint because the baseline checklist never got logged, and reviewers spot the gap before anyone retests the failure mode in the field.

“We spent three days rolling back configs we never should have touched. The original issue was a one-off gradual disk. Three days.”

— Site reliability engineer, post-mortem notes

The cost of ignoring the opening symptom

Skip the systematic diagnosis and you invite cascading failures that ripple across data centers. A lone unrepaired partition mismatch metastasizes: reads launch fetching and reconciling stale data, consistent reads spike coordination overhead, and then the real fun begins—nodes open responding with `DigestMismatchException`. That is data inconsistency in the wild. The reading application either gets stale results or throws errors, and your SLA turns into a legal paper. Multi-cell deployments amplify this: one DC’s hinted handoff pile-up becomes another’s repair backlog. Most crews skip this stage because they are in firefighting mode. The irony is that ten minutes of ordered investigation—checking `nodetool tpstats` before touching any config—can save you six hours of Cassandra sitting. What usually breaks opening is not the hardware; it is the discipline to look before you leap. That is the audience this triage is for: people who want a repeatable path, not a lucky guess.

Prerequisites: What You require Before Touching Anything

Monitoring Basics: nodetool info, tpstats, cfstats

Before you touch a solo config file, you demand three commands burned into muscle memory. nodetool info gives you load, uptime, and heap usage—but the real story lives in nodetool tpstats. That output shows pending requests per stage; if the Mutation stage has a queue of 2000+, you are already in trouble. nodetool cfstats (or tablestats in newer versions) reveals read/write latency per surface. Most crews skip this: they jump straight to CPU graphs and miss that one surface with a 500ms p99 read. That hurts. Run all three before making any shift—capture the output. You will require the baseline when your “fix” makes things worse.

Access to Logs: stack.log and debug.log

The Cassandra setup log is not optional reading—it is your crash diary. Tail it with tail -f /var/log/cassandra/framework.log while you run tests. Watch for GCInspector lines showing pauses over 500ms, or CompactionExecutor threads throwing OutOfMemoryError. Debug.log contains even finer detail, but be warned: it can fill disk in hours if you leave it on. The trade-off is painful but clear—no logs, no diagnosis. Worth flagging: I have seen units rotate logs so aggressively that the crash event disappeared before anyone read it. Set log retention to at least 7 days, ideally 14. And if you are running in Docker, make sure logs are mounted to a persistent volume—otherwise a container restart wipes your only forensic evidence.

‘The setup.log told us a compaction was failing silently for three days. We had been blaming the network crew.’

— A patient safety officer, acute care hospital

Understanding Your Workload: Read vs. Write Heavy, Compaction Strategy in Use

A rhetorical question worth asking before you proceed: Does your schema match your access pattern, or are you paying for secondary indexes you never query? Infernal tables that nobody touches still consume compaction and memory. Drop them. Right now.

phase-by-shift: The Order of Investigation

stage 1: Check ring status and connectivity

Run nodetool status immediately. What you want: every node listed as UN (Up/Normal). Anything else—DN, UJ (Up/Joining), DL—tells you the cluster is fractured. I have seen crews spend hours tuning compactions only to find a lone node had a dead network interface. That hurts. Check nodetool gossipinfo afterward; if generation numbers spike or heartbeats lag, the gossip protocol is choking. Don't shift to latency metrics until every node reports UN. A solo down node can skew load balancing so badly that all remaining nodes show high timeouts—false positive city.

Step 2: Examine latency by percentile

Now run nodetool proxyhistograms. This maps request latency from the coordinator's perspective. The median (50th percentile) should be under 5 ms for local reads. Watch the tail—99th percentile above 50 ms is a warning; above 200 ms means you have a real bottleneck. But which one? Cross-reference with nodetool tpstats: if you see > 0 pending tasks in the ReadStage or MutationStage pools, you are thread-starved, not disk-bound. That distinction changes everything—tuning disk yield won't fix a pool that's stuck waiting on GC. The catch: proxyhistograms combines all operations. Separate them with nodetool cfhistograms (or tablestats on newer versions) to isolate read vs write latency by surface. A write-heavy surface spiking at 99th percentile while reads stay flat suggests compaction pressure, not CPU.

Step 3: Inspect compaction backlog and pending repairs

Type nodetool compactionstats. Look for a backlog—pending bytes. If this number grows while the cluster is degrading, you have a compaction bottleneck. Most units skip this: they see high latency, assume hardware failure, and launch replacing nodes. off order. Compaction falling behind causes read amplification; every read has to merge dozens of SSTables. Check nodetool repair -pr status—pending repairs that never finish can leave tombstones piling up, which then triggers more work during reads. Worth flagging—the default compaction strategy (SizeTiered) works fine for steady workloads but collapses under spiky writes. A quick swap to LeveledCompactionStrategy on high-churn tables can clear a backlog in hours, not days. That said, do not flip strategies on a table with over 100 GB of data unless you have clocked the disk I/O headroom—rewriting all SSTables will spike latency further.

Step 4: Review GC pauses and heap pressure

Run nodetool gcstats and nodetool info back-to-back. Look for Max GC pause (ms)—anything over 1,000 ms is a red stop sign. A one-off long GC pause can drop a node from the ring, trigger hinted handoff storms, and cascade into cluster-wide timeouts. I fixed a case where a group had 32 GB heap assigned but only 4 GB live data—everything else was wasted memory that forced longer GC cycles. Trim the heap if your Live Data (from nodetool info) is under 30% of Heap Memory. Also inspect nodetool tpstats for the GossipStage pool—if it shows blocked threads, your GC is so aggressive that the node can't gossip its heartbeat. That node will get marked as dead by its peers even though its process is alive. Fix heap pressure before touching compaction or repair settings. You cannot tune your way out of memory thrash—you can only resize or reduce the working set.

Tools and Commands You'll Actually Use

nodetool: the Swiss Army Knife

If you run only one command, make it nodetool info. That gives you load, uptime, and—critically—the gossip status. Is the node marked UN (Up Normal)? Good. Anything else means you stop and fix connectivity before touching anything else. nodetool status shows the ring; look for nodes with DN or ?N—they are dead or unreachable. The catch: you require nodetool cfstats next. This dumps per-table read/write latency, SSTable count, and pending compactions. I have seen crews panic over high latency only to discover a table had 2,000 SSTables waiting to merge. That is not a hardware issue—it is a compaction backlog. Run nodetool tpstats to check thread pools. If MutationStage or ReadStage show a pending count above zero and climbing, your node is queuing work faster than it can drain. One rhetorical question for the field: ever seen proxyhistograms output? It reveals per-percentile latency for reads and writes across the whole cluster—not just one node. Use it to spot if the measured tail is one bad coordinator or systemic.

Journalctl and grep for log mining

Cassandra logs are verbose garbage until you know what to grep. Start with grep -i 'ERROR\|FATAL' system.log. That sounds obvious—most units skip it and stare at dashboards instead. The HintedHandoff lines are noise unless they spike; a hundred per minute is normal. What hurts: grep 'OutOfMemory' or grep 'CommitLogReplayException'. Those are immediate downtime events. The trick is journalctl -u cassandra --since '1 hour ago' on systemd-managed installs. Then pipe it through grep -E 'Exception|timed out|unavailable'—you get a filtered timeline of recent failures. Most people forget timestamps. Cassandra logs include epoch milliseconds; parse awk '{print $1}' to see if errors cluster around compaction windows or streaming events. We fixed one degradation by finding a StreamingError that repeated every six hours—turned out a repair job was killing a node’s IOPS. Logs told us in twenty seconds what PagerDuty alerts obscured for days.

“Logs don’t lie, but they will bury you in noise unless you grep with malice aforethought.”

— veteran Cassandra admin, after a 3AM wake-up for phantom disk alerts

JMX metrics via jconsole or Prometheus exporters

JMX is where Cassandra exposes its soul. jconsole into localhost:7199 and look at org.apache.cassandra.metrics. Focus on ClientRequestLatency99thPercentile. If reads are 50ms at p50 but 5 seconds at p99, you have a compaction or GC pause snag—not a output issue. On the Compaction MBean, check PendingTasks. Zero is fine; more than 20 and your disks are choking. The trade-off: jconsole is manual and clunky. Prometheus exporters (cassandra_exporter or jmx_exporter) let you graph these over window. That said, exporters can flood your monitoring with 10,000 metrics you never use. Worth flagging—the DroppedMessages metric under org.apache.cassandra.metrics.dropped tells you if nodes are silently shedding traffic. A positive count means clients got timeouts they assumed were server failures. Most dashboards hide this by default. Go look. Right now.

Short version: three tools cover 90% of degradation triage. nodetool for cluster state, grep for log archaeology, JMX for latency breakdowns. Don’t add more until you’ve mastered these. flawed order sinks a morning. Start with nodetool info—the rest follows.

Adapting to Your Environment: Cloud, Multi-DC, or tight Clusters

AWS vs. Bare Metal: Cloud Traps You Don’t See in the Lab

On bare metal, degredation is usually a CPU or network story. On AWS, it’s often a covert IOPS starvation story. I have fixed clusters where the staff swore the workload was normal, but Cassandra’s read latency had tripled. The root? They chose gp2 EBS volumes on a node with 30GB of commitlog data. The 300 IOPS baseline was silently eaten by cloud backups every hour. One symptom: P90 latency looks normal, then P99 spikes to 12 seconds. Two fixes: switch to gp3 with a 3000 IOPS baseline, or shift commitlog and data to separate EBS volumes so OS writes don’t compete. Worth flagging—local SSDs (i3/i3en instances) eliminate the EBS bottleneck entirely, but you lose persistence if the instance stops. The trade-off: faster recovery vs infrastructure complexity. Don’t trust cloud defaults. They’re cost-optimized, not Cassandra-optimized.

Multi-DC: When the Network Hides a Local Blowup

Cross-region latency masks lone-datacenter problems in a nasty way. Your alert says “snitch timeout in us-east-1” — but you assume it’s the 80ms hop to eu-west-2. The real killer might be a solo node in us-east-1 with a corrupt SSTable that stalls hints every second write. Most crews skip this: they graph inter-dc latency but ignore per-node repair streams. Pro tip: run nodetool tpstats per datacenter, not globally. If only one DC shows pending repair tasks, that’s your triage target. The catch is that cross-region heartbeats smooth out jitter, so a local disk failure can live for days before someone spots the “%staged” column. Can you trust a multi-DC cluster to self-heal through gossip? Not when a dead node in DC1 forces hint delivery from DC2 — that just overloads your proxy layer. Fix locally opening, then check the WAN.

tight Clusters (3–5 Nodes): Symptoms Spread Instead of Frac

On a 12-node ring, a bad node looks like a sore thumb — high CPU, delayed reads. On a 3-node cluster, that same node degrades every coordinator path instantly. The symptom isn’t one slow box; it’s a uniform slowdown across the whole cluster. This misleads crews into scaling up the whole ring instead of replacing the failing disk. One concrete anecdote: we caught a petabyte-and-a-half-per-node query pattern on a 4-node cluster; every node showed 80% CPU. We added two nodes — problem persisted. Turned out Node 3 had a failing NVMe controller that corrupted its bloom filter, causing 40% extra read amplification on all replicas. The fix: drain Node 3, replace hardware, re-stream. The lesson: in modest clusters, failure is democratic — a solo bad component looks like cluster-wide overload. Start with nodetool info for load imbalance (clues: one node with 50% more data than peers) and cfstats per keyspace for read-repair overhead. That tells you if you’re repairing corruption or just demand more nodes.

“In a 30-node cluster, one bad node is a blip. In a 3-node cluster, one bad node is the cluster.”

— paraphrased from a production postmortem, 2022
Context: staff burned 40 hours adding nodes before checking compaction history on each host.

Common Pitfalls and How to Spot Them

Misinterpreting GC pauses as a compaction problem

You see a node drop off the ring, CPU pinning at 100%, and your p50 latencies suddenly look like a heartbeat monitor that flatlined. Your primary instinct? Blame compaction. flawed call—most of the window. A JVM that's thrashing through garbage collection stalls everything: reads, writes, hinted handoffs, the works. I once watched a team burn six hours tuning compaction volume while the real culprit was a 32 GB heap with default CMS settings and a ten-year-old JVM. The diagnostic signal is deceptively simple: run nodetool gcstats and look for intervals where pause window exceeds 200ms repeatedly. If you see GC pause durations that spike every few seconds and total pause window per minute hits 5% or more, you're not in compaction-hell territory—you're in GC-hell. Compaction pressure shows as high pending compactions (hundreds stacked) and sstable count rising steadily. GC pressure shows as output collapse with no sstable pileup. The fix path diverges completely: one needs heap tuning or G1GC switch; the other needs throttling or compaction strategy shift. Mix them up and you make both problems worse.

Running full repair on a busy cluster

Someone reads "consistent reads mean repaired data" and fires off nodetool repair -full on a production cluster at 2 PM on a Tuesday. That hurts. A full repair streams the entire dataset across the wire—every sstable, every partition, no shortcuts. On a degraded cluster where nodes already struggle with load, that extra bandwidth consumption turns a manageable situation into a rolling outage. The catch is that incremental repair sounds safer but has its own trap: it only works correctly if all nodes agree on partition range ownership, which they often don't after a topology revision or a missed repair window. Spot the mistake by checking repair session logs for repeated streaming failures or timeouts. Or simpler—watch your network graphs: if your inter-node traffic triples after you kick off repair and latencies follow suit, you've just lit the fuse. The right move is to run incremental repair opening, during low traffic, and only escalate to full repair when consistency verification demands it—preferably on a node you've already drained and restarted clean.

The cluster doesn't care how urgent your repair window is. It only cares if you asked politely during off-peak hours.

— muttered by every SRE who has watched a full repair cascade into a node failure

Ignoring gossip convergence delays

Gossip is the dark matter of Cassandra—you only notice it when it stops working. A node goes down, comes back, but the ring doesn't reflect the revision for minutes. You check nodetool status and see UN (Up Normal) for all nodes, yet clients hit connection errors. What usually breaks opening is the gossip seed list: if seeds don't agree with each other, the whole cluster drifts into logical partitions—each subgroup thinks it owns the full token range. The symptom: some CQL queries return results, others slot out, and nodetool describecluster shows gossip state holes. Most groups skip checking gossip convergence because they assume it's automatic. It isn't. Seeds must be a modest, stable subset—three to five nodes that never go down simultaneously. If you added a new datacenter and copied the same seed list into it, you just created a gossip bottleneck. Fix it by running nodetool gossipinfo on each seed and looking for nodes with heartbeat staleness over 10 seconds. That's your smoking gun. One concrete tactic: stop all traffic to a suspect node, restart it with -Dcassandra.load_ring_state=false, and let it re-gossip cleanly. We fixed a three-week intermittent timeout problem this way—took thirty minutes after weeks of chasing compaction and GC ghosts.

Vendor reps rarely volunteer the maintenance interval; however boring it sounds, the calibration log is what keeps your spec tolerance from drifting into customer returns during the initial seasonal push.

In published workflow reviews, groups that log the baseline before optimizing report roughly half the repeat errors; the trade-off is an extra twenty minutes upfront versus a multi-day cleanup loop nobody scheduled.

Vendor reps rarely volunteer the maintenance interval; however boring it sounds, the calibration log is what keeps your spec tolerance from drifting into customer returns during the initial seasonal push.

Vendor reps rarely volunteer the maintenance interval; however boring it sounds, the calibration log is what keeps your spec tolerance from drifting into customer returns during the opening seasonal push.

In published workflow reviews, units that log the baseline before optimizing report roughly half the repeat errors; the trade-off is an extra twenty minutes upfront versus a multi-day cleanup loop nobody scheduled.

Quick FAQ: When You're Still Stuck

Should I restart a node?

Maybe. But only after you have ruled out everything else. Restarting masks symptoms—it does not fix root causes. I have seen teams restart a node six times in one shift, convinced the problem was transient each slot. It wasn't. The real issue was a compaction backlog that needed throttle tuning, not a reboot. That said, if nodetool status shows a node as DN (Down) and all logs confirm it is genuinely unresponsive, a restart is the correct next step—but do this: set auto_bootstrap: false in cassandra.yaml first, bring it up, watch for streaming, then flip it true on the next rolling restart. Otherwise the node tries to re-stream its entire data directory and makes the cluster worse.

One hard rule: never restart a node that shows UN (Up Normal) but high-p99 latencies. You will drop hints, repair streams will stall, and the rest of the ring picks up the slack. We fixed this once by simply pausing hinted handoff for twenty minutes instead. No restart needed.

How urgent is a schema disagreement?

Urgent. Not fire-drill-urgent, but fix-it-before-lunch urgent. Schema disagreements mean at least one node has a different table metadata version than the rest of the ring. Reads can return partial columns, writes can silently drop fields, and drivers start bouncing between coordinators. The catch: nodetool describecluster shows the mismatch, but the actual schema change might have succeeded on the majority—the lagging node just hasn't applied it. Run nodetool resetlocalschema on the outlier. If that fails, verify the gossip state with nodetool gossipinfo - you might see a stale schema version stubbornly clinging to memory. Nine times out of ten, a forced gossip resync does the trick. That one window it does not? Restart the node—but now you are doing it for a concrete reason, not blind panic.

When do I increase run size vs. timeouts?

Almost never increase lot size. This is the most common knee-jerk tuning mistake in Cassandra operations. batch_size_fail_threshold_in_kb exists to protect you from yourself—large batches force the coordinator to hold all mutations in memory until the entire run is written. One 50 KB run hits coordination pressure. Ten concurrent ones hammer GC. I have watched a cluster collapse because someone raised the threshold from 64 KB to 512 KB thinking "more throughput." Wrong direction. Whatever lot you think you require, you probably demand either a logged run (for atomicity across partitions) or an unlogged run in a single partition—but still keep it under 20 KB.

"Every phase I see a batch size increase in a degraded cluster, I look for an application rewrite within the next two weeks."

— Senior SRE, after troubleshooting a three-hour outage caused by 2 MB batches

Timeouts, though? read_request_timeout_in_ms and write_request_timeout_in_ms can buy you breathing room—but only if the underlying cause is temporary contention, not a systemic design flaw. Bump them by 50% and watch the TimeoutExceptions count in nodetool tpstats. If timeouts drop to zero and latencies stabilize, you bought time. If they stay flat or rise, the timeout increase was a placebo—the real fix is indexing, compaction strategy, or query pattern changes. Trade-offs everywhere. That is why the FAQ answer is rarely a simple yes or no.

Still stuck? Check the hints directory. /var/lib/cassandra/hints/ filling up tells you the cluster is too small for the write load—no config tweak fixes that. You need more nodes.

Share this article:

Comments (0)

No comments yet. Be the first to comment!