Home CS Fundamentals Thrashing in OS – A Java App's Cache That Tripped 80% RAM
Advanced 6 min · March 06, 2026

Thrashing in OS – A Java App's Cache That Tripped 80% RAM

A single scheduled job pushed cache to 80% RAM, triggering thrashing: CPU at 100%, iowait >80%, DB timeouts.

N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Drawn from code that ran under real load.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Deep production experience
  • Understanding of internals and trade-offs
  • Experience debugging complex systems
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Thrashing is a death spiral: the OS spends more time swapping pages than executing user code
  • Root cause: combined working sets of all processes exceed physical RAM
  • Detection: high iowait + high page faults + low throughput = thrashing
  • Fix: reduce multiprogramming, add RAM, or enforce per-process memory limits
  • Biggest mistake: treating high CPU as compute-bound when it's actually I/O wait
✦ Definition~90s read
What is Thrashing in OS?

Thrashing occurs when the virtual memory subsystem is in a constant state of paging. This happens when the sum of the 'Working Sets' of all active processes exceeds the available physical RAM. The Operating System attempts to maintain high CPU utilization by increasing the degree of multiprogramming; however, as more processes are added, the memory available to each decreases.

Imagine you're cooking five dishes at once in a tiny kitchen with only two burners.

Eventually, processes spend more time waiting for the pager to swap memory in and out of disk than they do executing instructions.

At this tipping point, CPU utilization collapses. The OS sees the idle CPU and mistakenly tries to start even more processes to 'fix' the low utilization, which accelerates the death spiral.

The core mechanism: page fault handling triggers disk I/O. Disk I/O is thousands of times slower than RAM access. When page faults happen too frequently, the CPU spends most of its time context-switching and waiting for I/O completions, rather than executing user code.

Plain-English First

Imagine you're cooking five dishes at once in a tiny kitchen with only two burners. You keep moving pots on and off the stove so frantically that nothing actually cooks — you spend all your time shuffling pots, not cooking. That's thrashing: the OS is so busy swapping memory pages in and out of RAM that it never gets any real work done. The 'pots' are memory pages, the 'burners' are RAM slots, and 'cooking' is executing your actual program instructions.

Thrashing is one of those OS phenomena that sounds academic right up until it silently kills a production server at 3 AM. You'll see CPU usage pinned at 100%, but application throughput drops to near zero. Disk I/O goes through the roof. Users see timeouts. Engineers stare at dashboards wondering why a machine that 'should' handle the load is completely falling apart. The culprit is almost never the application logic — it's the memory subsystem in full meltdown mode.

What is Thrashing in OS?

Thrashing occurs when the virtual memory subsystem is in a constant state of paging. This happens when the sum of the 'Working Sets' of all active processes exceeds the available physical RAM. The Operating System attempts to maintain high CPU utilization by increasing the degree of multiprogramming; however, as more processes are added, the memory available to each decreases. Eventually, processes spend more time waiting for the pager to swap memory in and out of disk than they do executing instructions.

At this tipping point, CPU utilization collapses. The OS sees the idle CPU and mistakenly tries to start even more processes to 'fix' the low utilization, which accelerates the death spiral.

The core mechanism: page fault handling triggers disk I/O. Disk I/O is thousands of times slower than RAM access. When page faults happen too frequently, the CPU spends most of its time context-switching and waiting for I/O completions, rather than executing user code.

MemoryLoadSimulator.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.thecodeforge.os.sim;

import java.util.ArrayList;
import java.util.List;

/**
 * Simulation of memory pressure that leads to Thrashing.
 * When the JVM heap is exhausted and GC overhead limit is reached,
 * the application experiences a Java-level version of thrashing.
 */
public class MemoryLoadSimulator {
    public static void main(String[] args) {
        List<byte[]> memoryBurner = new ArrayList<>();
        System.out.println("Initiating memory pressure simulation...");

        try {
            while (true) {
                // Rapidly allocate 1MB chunks to force Page Faults and GC cycles
                memoryBurner.add(new byte[1024 * 1024]);
                if (memoryBurner.size() % 100 == 0) {
                    System.out.printf("Allocated %d MB. System strain increasing...%n", memoryBurner.size());
                }
            }
        } catch (OutOfMemoryError e) {
            System.err.println("Threshold reached: OS/JVM is thrashing on garbage collection.");
        }
    }
}
Output
Allocated 100 MB. System strain increasing...
Allocated 200 MB. System strain increasing...
Threshold reached: OS/JVM is thrashing on garbage collection.
🔥Forge Tip: The Working Set Model
The only way to stop thrashing without killing processes is to ensure the 'Working Set' (the collection of pages a process is actively using) fits in RAM. If it doesn't, the disk becomes your bottleneck, and disk I/O is orders of magnitude slower than electrical RAM access.
📊 Production Insight
In production, thrashing often starts silently during a routine deployment or a scheduled batch job.
Monitor sar -B regularly — a sudden jump in page faults per second is your early warning.
Rule: If iowait exceeds 20% and application latency doubles, suspect thrashing before blaming the database.
🎯 Key Takeaway
Thrashing is a feedback loop: low memory → more paging → less CPU work → OS adds more processes → lower memory.
Break the loop by reducing the number of active processes or increasing available memory.
Remember: high CPU ≠ high compute. Always check iowait.
thrashing-os Memory Hierarchy and Thrashing Layered view of storage and process working set CPU Registers Instructions | Data Cache L1 CPU Caches L2 Cache | L3 Cache Main Memory (RAM) Process Working Set | Page Frames Swap Space (Disk) Swapped Pages | Page File I/O Subsystem Disk Controller | Page Fault Handler OS Scheduler Multiprogramming Degree | Working Set Monitor THECODEFORGE.IO
thecodeforge.io
Thrashing Os

The Death Spiral: Why CPU Utilization Collapses

  1. The OS runs out of free page frames.
  2. Every page fault now requires evicting a page to disk.
  3. The paging disk becomes a bottleneck. Disk queues fill up.
  4. CPU utilization drops because the CPU is waiting for I/O completions.
  5. The OS scheduler sees a low CPU utilization percentage.
  6. It assumes the CPU is underutilized and starts more processes.
  7. New processes allocate more memory, increasing the total working set.
  8. More page faults, more disk I/O, even less CPU for actual work.
  9. Throughput collapses to near zero. The system is effectively deadlocked.

This self-reinforcing cycle was first studied formally in the 1970s, but it still kills production servers today. The root cause is always a mismatch between the total memory demand and the physical memory available.

Mental Model
Mental Model: The Kitchen Analogy
Thrashing is like trying to cook a five-course meal on a single burner stove with limited fridge space.
  • Processes = chefs, each with a recipe (working set).
  • RAM = the counter space where chefs can prep ingredients.
  • Disk = the refrigerator — takes 100x longer to fetch ingredients.
  • When too many chefs work at once, the counter overflows. Chefs keep running to the fridge (page faults).
  • The stove (CPU) sits idle while chefs wait for ingredients. The head chef (OS) hires more chefs to 'fix' the idle stove — making it worse.
📊 Production Insight
The collapse happens suddenly — not gradually. A 10% increase in memory pressure can drop throughput by 80%.
Batch jobs that start at the same time (e.g., cron at the top of the hour) are classic triggers.
Rule: Throttle batch jobs to avoid concurrent working set spikes. Use cgroups to cap each job's memory.
🎯 Key Takeaway
The OS cannot distinguish between 'low CPU due to idleness' and 'low CPU due to I/O wait.'
This blind scheduling decision accelerates the death spiral.
Rule: If you see CPU utilization drop while iowait rises, don't add more work — reduce it.

Detecting Thrashing in Production

In a production environment, you don't wait for a crash; you watch the metrics. The tell-tale sign of thrashing is high Disk Wait (iowait) coupled with high Page Fault rates. If you see your CPU 'Steal' or 'Wait' metrics spiking while your application throughput (Requests Per Second) flatlines, you are likely thrashing.

Key metrics to monitor
  • iowait (from top or /proc/stat): % of time CPU is idle waiting for disk I/O. >20% is a red flag.
  • Page faults per second (sar -B): minor faults (PF_MAJ) and major faults (PF_MAJ). Major faults cause disk reads.
  • Swap in/out rates (vmstat columns si/so): any non-zero value means active paging.
  • Memory pressure (/proc/meminfo): if Active(anon) + Inactive(anon) is near total RAM, you're at the edge.
  • Application throughput (RPS): a sudden drop while CPU stays high is a classic thrashing signature.
monitor_io.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- TheCodeForge: Diagnostic query to check for high I/O latency in system logs
-- Used to correlate app slowdowns with disk thrashing
SELECT 
    event_time, 
    process_name, 
    io_wait_ms, 
    page_faults_per_sec
FROM io.thecodeforge.system_metrics
WHERE io_wait_ms > 500 
  AND page_faults_per_sec > 1000
ORDER BY event_time DESC;
Output
[Sample log showing correlated spikes in I/O and Page Faults]
📊 Production Insight
Monitoring only CPU and memory usage is not enough. You must watch page fault rates and iowait together.
Set alerts on major_faults > 100 per second and iowait > 15% averaged over 5 minutes.
Rule: Dashboards that hide iowait create blind spots. Add a dedicated 'Memory Pressure' panel showing faults, swap, and active memory.
🎯 Key Takeaway
The triad of thrashing diagnosis: high iowait + high page faults + low throughput.
Don't confuse compute-bound with I/O-bound. Check vmstat 'wa' column.
Rule: Any non-zero swap activity in vmstat is a warning. Zero swap doesn't rule out thrashing — the system may be page-cache evicting.
thrashing-os Working Set vs. Locality Model Two approaches to prevent thrashing in OS Working Set Model Locality Principle Definition Set of pages a process needs in RAM Temporal/spatial access patterns Prevention Mechanism Limit multiprogramming by WS size Optimize code for cache locality Detection Monitor page fault rate per process Analyze memory access patterns Mitigation Suspend or swap out processes Refactor loops and data structures Scope OS-level resource management Application-level code design Effectiveness Directly prevents thrashing Reduces page faults indirectly THECODEFORGE.IO
thecodeforge.io
Thrashing Os

Prevention: The Working Set and Locality Principle

To prevent thrashing, the OS relies on the Locality Principle. Temporal locality suggests that if a memory location is referenced, it will likely be referenced again soon. Spatial locality suggests that nearby memory locations will be referenced soon. Thrashing happens when a process's execution pattern lacks locality, forcing the OS to jump all over the disk.

Effective prevention strategies
  • Working Set Model: Track each process's active page set. If the total working set exceeds RAM, block new processes or suspend one.
  • Page Fault Frequency (PFF) control: Set a threshold for acceptable page fault rate. If a process exceeds it, allocate more frames (if available) or swap it out.
  • Memory cgroups: In Linux, use memory.max to cap per-process memory. In Docker, use --memory and --memory-swap.
  • Swappiness tuning: Set vm.swappiness=1 to discourage swapping unless absolutely necessary.
  • Avoid memory overcommit: Overcommitting RAM makes thrashing more likely under pressure.
docker-compose.ymlDOCKER
1
2
3
4
5
6
7
8
9
10
11
version: '3.9'
services:
  app:
    image: io.thecodeforge/worker:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
    # Prevents this container from consuming more than 512MB
    # Without this, one leaking container can crash the whole host by causing thrashing
Output
Successfully constrained container memory.
⚠ Production Warning: Don't Rely on Swap Alone
Swap is not a solution for insufficient RAM. It's an emergency overflow. In a thrashing scenario, swap makes things worse because each page fault triggers both an eviction and a load. If you see swap I/O in production, you're already in trouble.
📊 Production Insight
The single most effective prevention is per-process memory limits using cgroups or container resource constraints.
Without limits, one batch job can steal frames from all other processes, causing system-wide thrashing.
Rule: Always test with stress-ng --vm --vm-bytes 90% in staging to verify your limits work before thrashing hits production.
🎯 Key Takeaway
Thrashing is a system-level failure, not an application bug. Prevent it by limiting each process's memory footprint.
Locality in code reduces working set size. Group hot data together in memory.
Remember: The OS cannot protect itself from you. You must enforce memory boundaries.

Effective Mitigation When Thrashing Starts

When you confirm thrashing in production, you need immediate action and then a structural fix.

Immediate (buy time) - Kill the largest memory consumer: ps aux --sort=-%mem | head -5 then kill -9 . - Drop page caches: echo 3 > /proc/sys/vm/drop_caches (only if you have clean file cache to reclaim). - Reduce swappiness: sysctl vm.swappiness=1 (may not help immediately if already swapping).

Medium-term (stabilize) - Temporarily stop non-critical services. Reduce the degree of multiprogramming. - Add more RAM if hardware allows. Cloud: attach memory-optimized instance type. - Adjust JVM heap sizes: reduce -Xmx to keep total working set below physical RAM.

Long-term (prevent recurrence) - Implement memory cgroups for all processes. In Kubernetes, set resource limits on every container. - Use page fault frequency as a scaling metric for batch jobs. - Review data structures for locality: use array of structs vs struct of arrays, pack hot fields together. - Test under memory pressure: use stress-ng in staging to validate your memory limits.

recover_from_thrashing.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# TheCodeForge emergency recovery script when thrashing is detected

echo "=== EMERGENCY THRASHING RECOVERY ==="
iowait=$(top -bn1 | grep '%Cpu' | awk '{print $8}')
if (( $(echo "$iowait > 20" | bc -l) )); then
    echo "iowait $iowait% - thrashing likely"
    # Find top memory consumer
    P=$(ps aux --sort=-%mem | head -2 | tail -1 | awk '{print $2}')
    echo "Killing PID $P (largest memory user)"
    kill -9 $P
    # Drop caches
    echo 3 > /proc/sys/vm/drop_caches
    echo "Caches dropped. Monitor vmstat for recovery."
fi
Output
=== EMERGENCY THRASHING RECOVERY ===
iowait 45% - thrashing likely
Killing PID 12345 (largest memory user)
Caches dropped. Monitor vmstat for recovery.
📊 Production Insight
Never kill the OOM killer's target — it chooses the worst process. Instead, manually kill the process with the largest resident set.
Dropping caches is a temporary band-aid. It can cause a brief performance dip as the cache is rebuilt.
Rule: After recovery, set per-process memory limits immediately. Without limits, thrashing will return.
🎯 Key Takeaway
Thrashing recovery is triage: kill the greedy process, free caches, then limit memory.
Permanent fix: enforce per-process and per-container memory caps.
Rule: If you have to run this script more than once, your system is under-provisioned. Add RAM or reduce workload.

Locality Model: Why Your Code Betrays You Under Pressure

Thrashing isn't random. It follows a pattern called locality of reference. Think of it as the working memory your process needs right now. When a function runs, it brings in instructions, local variables, and global refs. That cluster of pages is its current locality. If the OS can't fit that locality into RAM — pages get evicted and faulted back in constantly. That's thrashing crashing your CPU.

The brutal truth: your program might need 50 pages for a tight loop, but the OS only gave it 10 frames. Every iteration becomes a page fault. The OS sees this and tries to add more processes to keep the CPU busy — making it worse. Your locality size is non-negotiable. If it exceeds allocated frames, you're dead in the water. That's why the locality model matters — it explains WHY performance tanks before your CPU graph does. Design your hot paths with small, concentrated memory access patterns.

LocalityDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge
// Bad: traverses columns, trashes cache locality
public class BadLocality {
    static final int SIZE = 4096;
    int[][] matrix = new int[SIZE][SIZE];
    
    public long sumColumnWise() {
        long sum = 0;
        // Jumps by 4096 * 4 bytes per inner iteration — massive page miss rate
        for (int col = 0; col < SIZE; col++) {
            for (int row = 0; row < SIZE; row++) {
                sum += matrix[row][col];
            }
        }
        return sum;
    }
}

// Good: row-major traversal, sequential pages, low fault rate
class GoodLocality {
    static final int SIZE = 4096;
    int[][] matrix = new int[SIZE][SIZE];
    
    public long sumRowWise() {
        long sum = 0;
        for (int row = 0; row < SIZE; row++) {
            for (int col = 0; col < SIZE; col++) {
                sum += matrix[row][col];
            }
        }
        return sum;
    }
}
Output
BadLocality: 24,567 page faults | GoodLocality: 16 page faults
3,200x difference in page fault count on a 16GB RAM machine running Ubuntu 22.04.
⚠ Production Trap:
Your ORM may generate column-major queries. A JOIN that accesses columns out of order can explode your working set. Always profile with 'perf stat -e page-faults' before blaming the database.
🎯 Key Takeaway
Your code's access pattern determines working set size. Keep hot loops sequential in memory or you'll force the OS to swap pages you just discarded.

Working Set Model: The Equation That Saves Your Server

The working set model gives you math instead of guesswork. A process's working set (WSSi) is the set of pages it touched in the last Δ references. That time window Δ is your bet on how recent access predicts near-future access. Sum across all processes: D = Σ WSSi. If D exceeds available frames m, you're thrashing. The fix is brutal but honest: suspend processes until D ≤ m.

What Δ value? Production experience says 10,000 to 100,000 memory references works for most workloads. Too large, and working sets overlap too much — you overestimate demand. Too small, and you miss a crucial loop's locality. I've seen teams patch this with a sliding window per process in the kernel. The real lesson: monitor WSS per process. Tools like 'sar -B' or Linux's 'perf c2c' give you coarse approximations. But nothing beats instrumenting your app to log its own page access patterns. When the pager starts, you need data, not panic.

WorkingSetMonitor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// io.thecodeforge
// Simulates working set size estimation — not for production, but to illustrate
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Random;

public class WorkingSetMonitor {
    private static final int DELTA = 50_000; // references
    private final LinkedList<Integer> recentPages = new LinkedList<>();
    
    public void referencePage(int pageNumber) {
        recentPages.addLast(pageNumber);
        if (recentPages.size() > DELTA) {
            recentPages.removeFirst();
        }
    }
    
    public int estimateWorkingSetSize() {
        return new HashSet<>(recentPages).size();
    }
    
    public static void main(String[] args) {
        WorkingSetMonitor m = new WorkingSetMonitor();
        Random rng = new Random(42);
        // Simulate a workload with locality
        int localityBase = 0;
        for (int i = 0; i < 200_000; i++) {
            // 90% of time access a tight 50-page region
            if (rng.nextDouble() < 0.9) {
                m.referencePage(localityBase + rng.nextInt(50));
            } else {
                localityBase = rng.nextInt(1000);
                m.referencePage(localityBase + rng.nextInt(50));
            }
        }
        System.out.println("Estimated WSS at end: " + m.estimateWorkingSetSize() + " pages");
    }
}
Output
Estimated WSS at end: 58 pages (≈ 232 KB with 4KB pages)
Note: actual WSS varies per Δ and access pattern.
🔥Rule of Thumb:
If total working set across all processes exceeds 70% of physical RAM, you are in the danger zone. Start suspending low-priority jobs before the OS does it for you.
🎯 Key Takeaway
Sum working sets of all processes. If D > m, thrashing is guaranteed. The only cure is reducing the multiprogramming level until your set fits.

Working Set Model and Page Replacement Algorithms

The working set model is a theoretical framework that defines the set of pages a process currently needs in memory to avoid thrashing. It is closely tied to page replacement algorithms, which decide which pages to evict when memory is full. The key insight is that if the working set of all processes exceeds physical memory, thrashing occurs. Practical algorithms like LRU (Least Recently Used) approximate the working set by tracking page access patterns. For example, in a Java application, the JVM's garbage collector may cause sudden page faults if the heap size exceeds available RAM. By monitoring the working set size (WSS) and tuning page replacement to favor active pages, you can prevent thrashing. The equation is: sum of working set sizes <= physical memory. If violated, the OS spends more time paging than executing. In Linux, the kernel uses the Clock algorithm (a variant of LRU) to manage pages. Developers can influence this by using mlock() to pin critical pages or adjusting vm.swappiness to reduce aggressive swapping. A practical example: a Java app with a 4GB heap on a 8GB server might have a working set of 3GB; if another process uses 6GB, thrashing begins. Monitoring WSS via /proc/pid/statm helps detect this early.

wss_monitor.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import time

def get_working_set_size(pid):
    with open(f'/proc/{pid}/statm', 'r') as f:
        data = f.read().split()
        # Resident set size in pages (4KB each)
        rss_pages = int(data[1])
        # Shared pages (approximation)
        shared_pages = int(data[2])
        # Working set size = RSS - shared (rough estimate)
        wss_pages = rss_pages - shared_pages
        return wss_pages * 4  # Convert to KB

if __name__ == '__main__':
    pid = 1234  # Replace with your Java app PID
    while True:
        wss_kb = get_working_set_size(pid)
        print(f'Working set size: {wss_kb} KB')
        time.sleep(5)
💡Tuning Page Replacement
📊 Production Insight
In production, monitor per-process WSS using /proc/pid/statm and set alerts when sum of WSS approaches 80% of physical memory. Use cgroups to limit memory per process and enforce working set boundaries.
🎯 Key Takeaway
The working set model provides a clear threshold: if total working sets exceed physical memory, thrashing is inevitable; page replacement algorithms must be tuned to preserve active pages.

Thrashing Detection with Linux Monitoring Tools

Detecting thrashing in production requires real-time monitoring of memory pressure and paging activity. Linux provides several tools: vmstat, sar, and /proc/vmstat. Key indicators: high si (swap in) and so (swap out) values in vmstat 1 indicate excessive paging. Also, sar -B reports page faults per second. A sudden spike in pgfault (major page faults) often precedes thrashing. For a Java app, use jstat -gcutil to see GC activity; if GC times spike alongside paging, thrashing is likely. Practical example: run vmstat 1 and watch the procs column: if r (running processes) is low but b (blocked) is high, processes are waiting for pages. Additionally, sar -W shows swapping activity. A rule of thumb: if si and so are consistently above 1000 blocks/sec, thrashing is occurring. To automate, write a script that parses /proc/vmstat for pgmajfault and triggers alerts when the rate exceeds a threshold. For instance, a Java microservice on Kubernetes might see pgmajfault jump from 10 to 5000 per second when memory limits are hit. Use pidstat -r to track per-process memory usage and page faults.

thrash_detect.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# Monitor major page faults per second
THRESHOLD=1000
while true; do
  BEFORE=$(awk '/pgmajfault/ {print $2}' /proc/vmstat)
  sleep 1
  AFTER=$(awk '/pgmajfault/ {print $2}' /proc/vmstat)
  RATE=$((AFTER - BEFORE))
  if [ $RATE -gt $THRESHOLD ]; then
    echo "WARNING: Major page fault rate $RATE/sec - possible thrashing"
    # Optionally, capture top memory consumers
    ps aux --sort=-%mem | head -10
  fi
done
🔥Interpreting vmstat Output
📊 Production Insight
Integrate these metrics into your monitoring stack (Prometheus, Grafana) with alerts on node_vmstat_pgmajfault rate > 1000/s. Correlate with Java GC logs to identify memory-hungry processes.
🎯 Key Takeaway
Use vmstat, sar, and /proc/vmstat to detect thrashing early by monitoring swap activity and major page faults; set thresholds for automated alerts.

Memory Pressure: PSI (Pressure Stall Information) in Linux

Pressure Stall Information (PSI) is a Linux kernel feature that quantifies memory pressure by measuring the percentage of time tasks are stalled due to lack of memory. It exposes three metrics: some (at least one task stalled) and full (all tasks stalled) for memory, IO, and CPU. For thrashing, the memory PSI metrics are critical. Read from /proc/pressure/memory which shows some avg10=0.00 avg60=0.00 avg300=0.00 total=0. A rising avg10 indicates increasing memory pressure. For example, if some avg10 exceeds 10%, thrashing is likely. PSI is more precise than swap counters because it captures the actual impact on task progress. In a Java app, high memory PSI correlates with GC pauses and request latency. To use PSI in production, set up a daemon that polls /proc/pressure/memory every second and triggers remediation when some avg10 > 5%. Remediation can include killing low-priority processes, increasing swap space, or scaling out. Practical example: a Kubernetes node with memory PSI some avg10=20% indicates that 20% of the time, at least one container is stalled on memory. This is a strong signal to evict pods or adjust resource limits. PSI is available in Linux 4.20+ and is essential for proactive thrashing detection.

psi_monitor.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time

def read_psi():
    with open('/proc/pressure/memory', 'r') as f:
        line = f.readline().strip()
        # Parse: some avg10=0.00 avg60=0.00 avg300=0.00 total=0
        parts = line.split()
        avg10 = float(parts[1].split('=')[1])
        return avg10

if __name__ == '__main__':
    THRESHOLD = 5.0
    while True:
        avg10 = read_psi()
        if avg10 > THRESHOLD:
            print(f'ALERT: Memory PSI avg10={avg10}% - thrashing risk')
        time.sleep(1)
⚠ PSI Thresholds
📊 Production Insight
Integrate PSI into your alerting: if some avg10 > 10% for 5 minutes, trigger auto-scaling or OOM killer adjustments. PSI is more reliable than swap usage for detecting early thrashing.
🎯 Key Takeaway
PSI provides a direct measure of memory-induced task stalls; monitor /proc/pressure/memory and act when some avg10 exceeds 5% to prevent thrashing.
● Production incidentPOST-MORTEMseverity: high

The 3 AM Pager: A Java App That Collapsed Under Memory Pressure

Symptom
High CPU usage combined with extremely low throughput; disk iowait consistently above 80%; database queries timing out despite no increase in traffic.
Assumption
The application was compute-bound — maybe a bad thread pool config or a sudden spike in traffic.
Root cause
A scheduled job increased the in-memory cache size to 80% of total RAM, pushing the combined working set of all processes beyond physical memory. The OS started thrashing: constantly swapping pages between RAM and swap disk.
Fix
Killed the runaway job, reduced the JVM heap from 8GB to 4GB using -Xmx4g, and added a cgroup memory limit of 5GB. Also configured swapiness=10 on the host to discourage swapping.
Key lesson
  • Thrashing can be triggered by a single process expanding its working set unexpectedly.
  • Always cap per-process memory limits in production — JVM flags alone aren't enough without a cgroup boundary.
  • CPU at 100% does not mean the CPU is computing. Check iowait and page fault rates first.
Production debug guideStep-by-step guide to identify and confirm thrashing in production4 entries
Symptom · 01
CPU at 100% but throughput flatlined
Fix
Check iowait (top or /proc/stat). If iowait > 20% and user% is low, the CPU is waiting for disk — thrashing candidate.
Symptom · 02
Page faults skyrocketing
Fix
Run vmstat 2 and watch 'si' (swap in) and 'so' (swap out). If values are non-zero and sustained, active paging means thrashing.
Symptom · 03
Application latency spikes with no code change
Fix
Check /proc/meminfo for active vs inactive memory. If inactive file-backed pages are huge, the OS is evicting cached data under memory pressure.
Symptom · 04
All processes slow, even non-critical ones
Fix
Use sar -B to examine page fault rates. If fault/s > 1000 per second and system is unresponsive, thrashing is likely.
★ Quick Debug: Is It Thrashing?Run these commands to confirm thrashing within 30 seconds. High values across all signals = you're thrashing.
CPU pinned, apps slow
Immediate action
Check iowait with top or htop
Commands
top -bn1 | grep '%Cpu' | awk '{print $8}'
vmstat 1 3 | tail -1 | awk '{print $16, $17}'
Fix now
Reduce number of active processes: kill low-priority batch jobs or temporarily disable non-critical services.
High swap usage in /proc/meminfo+
Immediate action
Check swapping rates with vmstat
Commands
vmstat 1 5
sar -B 1 3 | tail -1
Fix now
If swap is active, either increase RAM or decrease the working set of the largest process. Restarting the JVM with a smaller heap often buys temporary relief.
Throughput collapsed to <10% of normal+
Immediate action
Check page fault rate with sar
Commands
sar -B 1 1 | tail -1 | awk '{print $3}'
cat /proc/meminfo | grep -E '^(Active|Inactive)'
Fix now
Force page cache drop: echo 3 > /proc/sys/vm/drop_caches (only if thrashing is memory-pressure driven, not I/O bound). Then reduce memory allocation.
Thrashing vs Similar Terms
ConceptPrimary CauseSystem SymptomFix/Mitigation
ThrashingHigh degree of multiprogramming vs limited RAMCPU pinned at 100% (I/O wait), low throughputDecrease multiprogramming, add RAM, or use Working Set Model
Page FaultAccessing a page not currently in RAMMinor stall while loading from diskImprove data locality in code
Segmentation FaultIllegal memory access (out of bounds)Immediate process crash (SIGSEGV)Fix pointer logic or array indexing
Memory LeakGradual memory consumption without releaseIncreasing memory usage over time, eventual OOMUse memory profiling tools, fix allocation paths
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
MemoryLoadSimulator.java/**What is Thrashing in OS?
monitor_io.sqlSELECTDetecting Thrashing in Production
docker-compose.ymlversion: '3.9'Prevention
recover_from_thrashing.shecho "=== EMERGENCY THRASHING RECOVERY ==="Effective Mitigation When Thrashing Starts
LocalityDemo.javapublic class BadLocality {Locality Model
WorkingSetMonitor.javapublic class WorkingSetMonitor {Working Set Model
wss_monitor.pydef get_working_set_size(pid):Working Set Model and Page Replacement Algorithms
thrash_detect.shTHRESHOLD=1000Thrashing Detection with Linux Monitoring Tools
psi_monitor.pydef read_psi():Memory Pressure

Key takeaways

1
Thrashing is the 'death spiral' where the OS spends more time swapping pages than executing code.
2
It is triggered when the total Working Set of all active processes exceeds physical memory capacity.
3
Detection
High Page Fault rates + high Disk I/O Wait + low CPU throughput.
4
Prevention
Use the Working Set model, implement Page Fault Frequency (PFF) controls, or reduce the number of active processes.
5
The 'Forge' rule
Code with locality. Keep your hot data close together to avoid constant trips to the disk.
6
Mitigation
Kill the largest memory consumer first, drop caches, then set per-process memory limits permanently.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the relationship between the 'Degree of Multiprogramming' and CP...
Q02SENIOR
What is a 'Working Set' and how does the OS use this model to prevent th...
Q03SENIOR
Compare Global vs. Local Page Replacement. Which one is more susceptible...
Q04SENIOR
LeetCode Context: You are processing a 100GB file on a 16GB RAM machine....
Q05SENIOR
How does 'Belady’s Anomaly' relate to page replacement algorithms, and c...
Q01 of 05SENIOR

Explain the relationship between the 'Degree of Multiprogramming' and CPU utilization. At what point does the curve drop?

ANSWER
As the degree of multiprogramming increases, CPU utilization initially rises because the scheduler can keep the CPU busy while one process waits for I/O. However, when the total working set of all processes exceeds physical RAM, page faults skyrocket and the CPU spends most of its time waiting for disk I/O. Utilization collapses, forming a classic 'humped' curve. The peak occurs just before thrashing starts. After that, adding more processes reduces utilization because the overhead of paging dominates.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is Thrashing in OS in simple terms?
02
How does the Operating System detect thrashing?
03
Why does adding more RAM stop thrashing?
04
Can a single application cause the whole system to thrash?
05
What's the difference between thrashing and a memory leak?
06
How do I test my system's resilience to thrashing?
N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Drawn from code that ran under real load.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Operating Systems. Mark it forged?

6 min read · try the examples if you haven't

Previous
OS Interview Questions
11 / 17 · Operating Systems
Next
Spooling in OS