home
diamond Go Premium
Data Engineering Path  ·  PySpark

RDD - Accumulator Variables

When writing a distributed application, you often need to track global metrics across the entire clustersuch as counting the number of blank or malformed lines in a 10TB dataset, tracking specific transaction types, or measuring error frequencies.

If you attempt to use a standard Python global variable inside your Spark operations, it will fail because executors run in separate JVM processes and cannot send updates back to the Driver program.

To solve this, Spark provides Accumulator Variablesshared variables that worker tasks can only write to (via addition), and only the Driver program can read.

This guide provides a detailed exploration of Accumulators, their execution rules, and the common lazy evaluation pitfalls to avoid.


1. Why Standard Python Variables Fail

Let's look at a common mistake:

# WARNING: THIS WILL NOT WORK!
counter = 0

rdd = sc.parallelize([1, 2, 3, 4])

def increment_count(x):
    global counter
    counter += 1 # Increments local copy on the executor JVM!
    return x

rdd.map(increment_count).collect()

print("Global Counter:", counter) 
# Output: Global Counter: 0 (The driver's counter is completely unaffected!)

The Accumulator Solution

An Accumulator acts as a secure, distributed collector. Spark takes care of initializing a local accumulator copy on each executor, gathering their final values automatically, and merging them at the Driver program using standard addition.

graph TD
    subgraph Driver["Driver Program (Master Node)"]
        A["sc.accumulator(0)"] -->|Deploy copy| E1["Executor 1 (Counter=0)"]
        A -->|Deploy copy| E2["Executor 2 (Counter=0)"]
        E1 -->|Adds +1, +1| E1_val["Final Local: 2"]
        E2 -->|Adds +1| E2_val["Final Local: 1"]
        E1_val & E2_val -->|Automatic Merge| A_final["Final Driver Value: 3"]
    end

    style Driver fill:#e1f5fe,stroke:#039be5,stroke-width:2px;

2. PySpark Code Examples

Setup Spark Session

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("Day01 Accumulator Variables") \
    .master("local[*]") \
    .getOrCreate()

sc = spark.sparkContext

A. Counting Malformed Rows in a Log File

Let's parse a dataset of logs and use an Accumulator to count the number of invalid rows without triggering an expensive network shuffle:

# 1. Initialize a numeric Accumulator starting at 0
malformed_rows_counter = sc.accumulator(0)

# 2. Raw logs dataset (some rows are empty or formatted incorrectly)
raw_logs = sc.parallelize([
    "2026-05-23 INFO: Login success",
    "", # Malformed: Empty row
    "2026-05-23 ERROR: Disk full",
    "INVALID_FORMAT_ROW", # Malformed: Missing log level
    "2026-05-23 INFO: Logging out"
], numSlices=2)

# 3. Parsing function that increments accumulator on error
def parse_logs(line):
    # Reference the global accumulator
    global malformed_rows_counter

    # Check if empty row
    if not line.strip():
        malformed_rows_counter.add(1) # Increment the accumulator by 1
        return None

    parts = line.split(" ")
    if len(parts) < 3:
        malformed_rows_counter.add(1) # Increment the accumulator by 1
        return None

    timestamp = parts[0]
    log_level = parts[1].replace(":", "")
    message = " ".join(parts[2:])

    return (timestamp, log_level, message)

# 4. Apply the map transformation (lazy evaluation!)
parsed_logs_rdd = raw_logs.map(parse_logs)

# 5. Filter out the None results
valid_logs_rdd = parsed_logs_rdd.filter(lambda row: row is not None)

# 6. Accumulators are LAZY! Value is still 0 before an Action
print("Counter before Action:", malformed_rows_counter.value) # Output: 0

# 7. Trigger the computation using an Action (collect)
valid_logs = valid_logs_rdd.collect()

print("
--- Processing Completed ---")
print("Valid Parsed Logs:")
for log in valid_logs:
    print(f"  {log}")

# 8. Query the accumulator's final merged value on the Driver
total_malformed = malformed_rows_counter.value
print("
Malformed Rows Counted Globally:", total_malformed)

# Expected Output:
# Counter before Action: 0
# Valid Parsed Logs:
# ('2026-05-23', 'INFO', 'Login success')
# ('2026-05-23', 'ERROR', 'Disk full')
# ('2026-05-23', 'INFO', 'Logging out')
#
# Malformed Rows Counted Globally: 2

3. The Lazy Transformation Gotcha: Double-Counting

Because Spark transformations are lazy, updates to accumulators inside transformations (like map or filter) can be unreliable if you execute multiple actions on the same RDD.

The Double-Counting Scenario:

If you run rdd.collect(), the accumulator is updated. If you run rdd.count() next, Spark recomputes the entire lineage graph from scratch, causing the accumulator to run a second time and double its count!

# 1. Create a fresh accumulator
double_counter = sc.accumulator(0)

# 2. Lazy transformation that increments the accumulator
numbers_rdd = sc.parallelize([1, 2, 3]).map(lambda x: double_counter.add(1))

# 3. Action 1: Triggers map (increments counter to 3)
numbers_rdd.collect()
print("Accumulator after Action 1:", double_counter.value) # 3

# 4. Action 2: Triggers map AGAIN (increments counter to 6!)
numbers_rdd.count()
print("Accumulator after Action 2 (Double-counted!):", double_counter.value) # 6

How to Prevent Double-Counting:

  1. Use Actions for Updates: Increment accumulators inside the foreach(func) action. Spark guarantees that each record will be processed exactly once, and re-execution will not occur unless node failures force it.
  2. Apply Caching: Call .cache() on the RDD before running your first action. Subsequent actions will read directly from the cached RAM, bypassing the transformation lineage and preventing re-increments.

4. Key Rules for Accumulators

  1. Write-Only on Executors: Worker tasks can only write to the accumulator using the .add() method. They cannot read the accumulator's value. Attempting to query malformed_rows_counter.value inside a map/filter transformation will throw a runtime compilation error!
  2. Read-Only on Driver: The Driver program is the only process that can read the accumulator value using .value.
  3. Custom Accumulators: Spark allows you to create custom accumulators for non-numeric data types (e.g., aggregating lists or custom classes) by extending the AccumulatorParam class.
Find this content helpful? ☕ Buy me a coffee

Entity Details

Create New Item

celebration
Enjoying the free content?

Create a free account to track your progress and save your place.

Create Free Account
help

Submit Technical Query

Have a question or run into an issue? Describe it below, upload an optional screenshot, and our engineering team will answer it!

image Attach image (optional)

Submit Feedback

build Free Developer Utility Free Tool
gavel

Privacy & Legal Disclaimer

1. Client-Side Browser Processing

All utility tools on DeepEngineerHub (including Image to PDF, Text Formatters, JSON Converters, and Encryptors) execute 100% locally within your client browser using WebAssembly and JavaScript. No uploaded images, text, or documents are transmitted, collected, or stored on remote servers.

2. Limitation of Liability ("As-Is" Provision)

Tools and services are provided free of charge for convenience and educational purposes "as-is" without warranties of any kind. DeepEngineerHub shall not be held liable for any data loss, formatting inconsistencies, or indirect damages resulting from tool usage.

3. Open Source & Third-Party Software

Certain utilities utilize open-source client libraries (such as jsPDF, Mermaid.js, Pyodide) licensed under MIT, Apache, or BSD open licenses. All intellectual property remains with their respective copyright holders.