Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›PySpark›Caching and Persistence

Performance

Caching and Persistence

Caching and persistence allow you to store the results of intermediate transformations in memory or on disk to avoid recomputing them during subsequent actions. By strategically caching, you prevent redundant expensive lineage re-evaluations, significantly accelerating iterative workloads. You reach for these techniques whenever a specific DataFrame is accessed multiple times within a single application flow.

The Logic of Lazy Evaluation and Recomputation

In PySpark, transformations are lazy, meaning the execution plan is only built when an action like collect() or write() is triggered. Without caching, every time you call an action on a DataFrame, the entire lineage—every transformation back to the source data—is re-executed from scratch. Imagine you have a massive join operation that takes several minutes; if you perform five separate count or filter operations on the result of that join, PySpark will perform the join five separate times. This redundant computation is a common performance bottleneck in complex pipelines. Caching acts as a checkpoint within the physical execution plan. When you invoke a caching command, you are telling the cluster to retain the processed partitions in memory after the first successful computation. Future actions can then bypass the initial transformation steps, retrieving the data directly from the executor's storage instead of re-reading or re-processing input sources.

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()
# Create a large DataFrame from an existing source
df = spark.read.parquet("large_dataset.parquet")
# Transformation logic: if cached, this is done once
filtered_df = df.filter(df['revenue'] > 1000).cache()
# First action: computes the filter
print(filtered_df.count())
# Second action: retrieves cached result from memory
filtered_df.show(5)

Memory Storage Levels and Volatility

Caching is not a one-size-fits-all setting; the StorageLevel defines how the data is preserved. The default method, .cache(), is equivalent to MEMORY_ONLY. This stores the DataFrame as deserialized Java objects in the JVM heap space. If the dataset size exceeds the available memory in the cluster, partitions are dropped and recomputed as needed, which can cause severe performance degradation. Understanding the trade-offs is vital: while deserialized storage is extremely fast for subsequent access, it consumes significant memory. If memory pressure is a concern, you might consider serialized options. Serialized caching converts the DataFrame into a dense byte array, consuming much less heap space at the cost of slight overhead during retrieval when the data must be deserialized back into objects. Choosing the right storage level requires balancing your cluster's total memory against the frequency of access and the complexity of recomputing the data partitions.

from pyspark import StorageLevel

# Cache in memory as serialized objects to save space
# This is more efficient for limited memory environments
df = spark.read.parquet("large_dataset.parquet")
df.persist(StorageLevel.MEMORY_ONLY_SER)
# Compute once and store for later iterations
result = df.groupBy("region").sum("sales").collect()

When to Use Persistence over Caching

While cache() is a convenient alias, persist() offers more control over the lifecycle of your data. Persistence allows you to define exactly where the data lives, including options like MEMORY_AND_DISK or DISK_ONLY. The disk-based persistence levels are particularly useful when you have a dataset that is too large to fit in memory but is still expensive to recompute. By spilling to disk, you avoid the cost of re-reading source files or re-running heavy window functions, even if memory is exhausted. This provides a safety net against OOM (Out of Memory) errors while still providing substantial speedups compared to repeating transformations. Always evaluate your memory constraints before caching; if your cache is constantly being evicted, you are likely suffering from a 'thrashing' effect, where the time spent moving data in and out of memory outweighs the gains from avoiding the re-computation of the lineage.

# Use disk to avoid OOM if the dataset is massive
# This survives node memory pressure better than plain .cache()
large_df = spark.read.parquet("massive_dataset.parquet")
large_df.persist(StorageLevel.MEMORY_AND_DISK)
# Now we perform multiple complex analytical passes
summary = large_df.agg({"val": "mean"}).collect()
report = large_df.groupBy("category").count().collect()

Understanding Cache Eviction and Unpersisting

It is a common mistake to assume that cached data stays in memory indefinitely. PySpark uses an LRU (Least Recently Used) cache policy; when the cluster runs low on memory, it will evict older or unused cached partitions to make room for new data. Furthermore, caching is local to the current Spark application; once the SparkSession terminates, the cached data is released. You must be proactive in managing these resources. If you no longer need a DataFrame, calling unpersist() is a best practice. Leaving large, unused objects in memory blocks the garbage collector and prevents other tasks from utilizing that space, potentially slowing down the entire cluster. By explicitly clearing memory once a pipeline stage is complete, you allow the Spark scheduler to allocate memory to other pending tasks more effectively, resulting in a more stable and efficient execution flow for the entire job.

df = spark.read.parquet("data.parquet").cache()
df.count()
# Once the final report is generated, remove from cache
# This frees up heap space for subsequent pipeline stages
df.unpersist()
# Verify that the memory is released by checking internal storage state

Measuring Performance Impact with the UI

The only way to confirm that your caching strategy is effective is by monitoring the Spark UI. Under the 'Storage' tab, you can view which DataFrames are currently cached, the storage level used, and the percentage of data residing in memory versus disk. If you see '0% in Memory', it indicates that your cache is constantly being dropped or that you have underestimated your data volume. A successful cache implementation should show high 'Memory Usage' and a significant reduction in task duration for subsequent actions. Look for the 'Storage' column in the 'Stages' view; tasks that read from the cache will show significantly shorter execution times compared to the initial stage that performed the disk-based shuffle or scan. Use these metrics to determine if you need to increase your executor memory or if a different serialization level would better suit your specific hardware environment and dataset characteristics.

# Code to identify if cache is actually working
# After caching, the 'Storage' tab in the Spark UI
# should show the cached DataFrame and its memory size
df = spark.read.parquet("data.parquet").cache()
# Trigger execution to populate the storage
df.count()
# Monitor via spark.sparkContext._jsc.sc().getPersistentRDDs()

Key points

  • Caching prevents redundant recomputation of DataFrame lineage by storing results in memory or on disk.
  • The default .cache() method uses the MEMORY_ONLY storage level, which is fast but can cause memory pressure.
  • Persist provides additional storage levels like MEMORY_AND_DISK for scenarios where data exceeds available memory.
  • Lazy evaluation is the reason why caching only takes effect when a physical action is triggered.
  • Serializing cached data consumes less memory but introduces a slight overhead during the deserialization phase.
  • Always use unpersist() to release cluster resources once a specific intermediate DataFrame is no longer needed.
  • The LRU cache policy in PySpark automatically evicts partitions if memory limits are exceeded during execution.
  • The Spark UI Storage tab is the primary tool for verifying whether your data is actually being cached successfully.

Common mistakes

  • Mistake: Calling .cache() on every DataFrame unnecessarily. Why it's wrong: Caching forces data into memory, which can lead to OOM errors if the working set is larger than available RAM. Fix: Only cache DataFrames that are reused multiple times in iterative algorithms or complex branching logic.
  • Mistake: Expecting .cache() to trigger immediate execution. Why it's wrong: Caching in PySpark is lazy; the data is only cached when an action is triggered. Fix: Follow .cache() with a count() or similar action if you need to ensure the data is loaded into memory before further processing.
  • Mistake: Misunderstanding the difference between .cache() and .persist(). Why it's wrong: Users assume they are identical, ignoring that .persist() allows specification of storage levels like DISK_ONLY or MEMORY_AND_DISK. Fix: Use .persist() when you need to fine-tune resource usage based on data size and recompute cost.
  • Mistake: Persisting data that is computed from a small, static source. Why it's wrong: The overhead of managing the cache blocks in the Spark block manager often exceeds the cost of recomputing the data. Fix: Only cache data if the lineage is deep or the transformation is computationally expensive.
  • Mistake: Failing to call .unpersist() after a cached DataFrame is no longer needed. Why it's wrong: Cached data remains in memory until the application terminates or the memory is reclaimed by LRU policies, potentially blocking other tasks. Fix: Explicitly call .unpersist() once you have finished the final stage of processing for that DataFrame.

Interview questions

What is the primary difference between the cache() and persist() methods in PySpark?

In PySpark, cache() is essentially a shorthand method that calls persist() with the default storage level, which is set to MEMORY_ONLY. The persist() method, however, provides the flexibility to specify different storage levels, such as MEMORY_AND_DISK or DISK_ONLY. You would use persist() when you need to control how the data is stored based on memory constraints, whereas cache() is used for simple, standard caching needs.

Why is it recommended to cache a PySpark DataFrame if it is used multiple times in an application?

PySpark is inherently lazy, meaning transformations are not computed until an action is triggered. If you reference a DataFrame multiple times in a sequence of actions, PySpark will re-evaluate the entire lineage of transformations from the source data every time. Caching materializes the DataFrame in memory or on disk after the first action. This prevents redundant re-computation of the execution graph, which significantly improves overall performance for iterative algorithms or multiple analytical queries on the same dataset.

When should you choose MEMORY_AND_DISK over MEMORY_ONLY for persisting a PySpark DataFrame?

You should choose MEMORY_AND_DISK when your dataset is larger than the available executor memory. If you use MEMORY_ONLY and the data exceeds memory capacity, PySpark will have to recompute partitions that were evicted. By using MEMORY_AND_DISK, PySpark spills the overflow partitions to disk instead of recomputing them. This effectively trades a small latency increase from disk I/O for the much larger cost of re-calculating the entire RDD lineage from scratch.

What happens under the hood when you call unpersist() on a cached DataFrame?

When you call unpersist(), PySpark signals the BlockManager on the executors to drop the cached blocks associated with that specific DataFrame. This removes the data from memory or disk, freeing up resources for other tasks. This is a critical step in managing cluster memory effectively; if you fail to unpersist data that is no longer needed, you risk encountering out-of-memory errors because your Spark application will continue to hold onto stale data blocks.

Compare the performance trade-offs between using checkpoint() and persist() in a long-running PySpark job.

While both methods save data, they serve different purposes. Persist() keeps data in memory or local disk, but it maintains the lineage, meaning if an executor fails, Spark may need to recompute the data. Checkpoint() saves the data to a reliable, distributed file system like HDFS or S3 and breaks the lineage entirely. Checkpoint is slower because of the network overhead to write to HDFS, but it provides much stronger fault tolerance for extremely long-running iterative pipelines where recomputing lineage is too expensive.

Explain how serialization storage levels like MEMORY_ONLY_SER impact performance and memory usage in PySpark.

Using MEMORY_ONLY_SER forces PySpark to serialize each object into a byte array before storing it. This approach significantly reduces the memory footprint because serialized data is much more compact than raw Java or Python objects, often allowing you to fit more data into the heap. The trade-off is higher CPU usage, as the executor must deserialize the data back into objects every time it needs to perform an operation. This is ideal when memory is the primary bottleneck but you have sufficient CPU overhead to manage the serialization-deserialization cycles.

All PySpark interview questions →

Check yourself

1. Which scenario provides the most significant performance benefit when using .cache()?

  • A.A DataFrame that is transformed once and then saved to S3.
  • B.A DataFrame that is small enough to fit into a single driver node memory.
  • C.An iterative algorithm like PageRank that repeatedly accesses the same DataFrame.
  • D.A DataFrame created by reading a single small CSV file from local disk.
Show answer

C. An iterative algorithm like PageRank that repeatedly accesses the same DataFrame.
Caching is beneficial for iterative algorithms where the same data is scanned multiple times. Option 1 is wrong because writing once doesn't require caching. Option 2 is wrong because the driver memory isn't the primary concern for distributed tasks. Option 4 is wrong because the overhead of caching outweighs the cost of reading a small file.

2. When you call .persist(StorageLevel.DISK_ONLY), what happens to the execution flow?

  • A.The data is immediately serialized and written to the executor's local disk.
  • B.The data is marked for persistence, and will be written to disk only when an action is performed.
  • C.The data is written to the HDFS master node for permanent storage.
  • D.The Spark context immediately halts to flush the current DAG to disk.
Show answer

B. The data is marked for persistence, and will be written to disk only when an action is performed.
Spark operations are lazy; persistence only takes effect upon the next action. Option 1 is wrong because it isn't immediate. Option 3 is wrong because DISK_ONLY refers to local executor disks. Option 4 is wrong because Spark does not halt for persistence.

3. What is the primary consequence of failing to call .unpersist() on a very large, cached DataFrame?

  • A.The program will immediately crash with a StackOverflowError.
  • B.Spark will automatically remove the oldest cached item regardless of usage.
  • C.The application may experience OutOfMemory errors in subsequent stages due to reduced available heap space.
  • D.The data will be automatically copied to the driver, causing network congestion.
Show answer

C. The application may experience OutOfMemory errors in subsequent stages due to reduced available heap space.
Cached data consumes memory. If not cleared, it crowds out space needed for new tasks. Option 1 is incorrect as memory issues manifest as OOMs, not stack errors. Option 2 is a partial truth regarding eviction but doesn't explain the performance impact. Option 4 is wrong because cached data stays on executors.

4. If a cached RDD or DataFrame partition is lost, how does Spark handle it?

  • A.The entire job fails immediately.
  • B.Spark uses the lineage information to recompute only the lost partition.
  • C.Spark halts and requests the user to manually trigger a re-cache.
  • D.The missing data is silently ignored and treated as empty.
Show answer

B. Spark uses the lineage information to recompute only the lost partition.
Spark's fault tolerance is built on recomputing only the lost parts of the graph. Option 1 is wrong because Spark is designed for resilience. Option 3 is wrong because user intervention is not required. Option 4 is wrong because data integrity must be maintained.

5. Why is it generally discouraged to cache the result of a very simple, fast transformation?

  • A.The serialization and deserialization overhead can be more expensive than the original computation.
  • B.Caching simple operations increases the risk of data corruption.
  • C.Simple transformations cannot be cached in PySpark due to API limitations.
  • D.Caching always forces the data to be stored on the driver, creating a bottleneck.
Show answer

A. The serialization and deserialization overhead can be more expensive than the original computation.
Caching involves overhead (serialization, object management). For fast operations, it is cheaper to recompute. Option 1 is the correct rationale. Option 2 is false. Option 3 is false. Option 4 is false because caching happens on executors.

Take the full PySpark quiz →

← PreviousPartitioning and ShufflingNext →Broadcast Variables and Accumulators

PySpark

26 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app