MapReduce splits a job into map (filter/sort) and reduce (aggregate) phases, each run in parallel. Use it when you need to process terabytes of data that won't fit on one machine. Don't use it for real-time or small datasets — the overhead will kill you.
✦ Definition~90s read
What is MapReduce and Batch Processing?
MapReduce is a programming model for processing large datasets in parallel across a cluster. Batch processing is the execution of non-interactive, data-intensive jobs on a schedule. Together they form the backbone of offline data pipelines.
★
Imagine you're a librarian asked to count every word in a million books.
Plain-English First
Imagine you're a librarian asked to count every word in a million books. You don't read them one by one — you hand each book to a different person (map phase) who writes down word counts for their book. Then you collect all those lists and add up the totals (reduce phase). That's MapReduce. Batch processing is like doing this every night after the library closes, not while patrons are browsing.
I've seen a 200-node Hadoop cluster brought to its knees because someone ran a join without a partitioner. The job ran for 14 hours, then failed with a shuffle error. The fix was a single config change. That's the kind of thing this article will save you from. MapReduce isn't dead — it's just hiding inside Spark, Flink, and every cloud data warehouse. If you don't understand the core model, you'll misconfigure your Spark jobs and wonder why your 100-node cluster is slower than a single laptop. By the end of this, you'll know exactly when to use batch processing, how to design a MapReduce job that won't fall over, and — more importantly — when to tell your boss that a simple SQL query is the right answer.
Why MapReduce Exists: The Problem Before Parallelism
Before MapReduce, processing a terabyte of data meant either buying a supercomputer or writing custom distributed code with sockets and locks. Both were expensive and fragile. The core insight of MapReduce is simple: if you can express your computation as a map (apply a function to each record independently) followed by a reduce (aggregate results by key), you get automatic parallelism, fault tolerance, and data locality. The 'why' is that it hides all the distributed systems horror — node failures, network partitions, stragglers — behind a clean abstraction. Without it, every data pipeline would be a bespoke mess of MPI calls and manual checkpointing.
WordCount.systemdesignSYSTEMDESIGN
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
// io.thecodeforge — SystemDesign tutorial
// Word count: the canonical MapReduce example, but with production framing.
// Input: 10TB of web server logs. Output: word frequency across all pages.
// Map phase: emit (word, 1) for each word in a line
function map(line) {
const words = line.toLowerCase().split(/\W+/);
for (let word of words) {
if (word.length > 0) {
emit(word, 1); // key: word, value: 1
}
}
}
// Reduce phase: sum all counts for each word
function reduce(word, counts) {
let total = 0;
for (let count of counts) {
total += count;
}
emit(word, total);
}
// The framework handles partitioning, sorting, shuffling, and fault tolerance.
// Output: sorted list of (word, frequency) pairs.
Output
the 1234
and 987
of 876
a 654
...
🔥Senior Shortcut:
If your map function doesn't produce key-value pairs, you don't need MapReduce. You need a parallel for-loop. MapReduce is only useful when you need to group data by key across nodes.
thecodeforge.io
Mapreduce Batch Processing
The Map Phase: Splitting Work Without Splitting Hairs
The map phase reads input splits (typically HDFS blocks of 128MB) and applies your map function to each record. The output is a list of intermediate key-value pairs. The framework then partitions these by key (default: hash(key) % numReducers) and writes them to local disk. This is where most performance problems start. If your map output is too large, you'll spill to disk repeatedly. The fix is a combiner — a mini-reducer that runs on the map side to aggregate data before the shuffle. For example, in word count, the combiner sums counts per mapper, reducing the data sent over the network by 90%.
MapWithCombiner.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — SystemDesign tutorial
// Map with combiner for word count
function map(line) {
const words = line.toLowerCase().split(/\W+/);
const localCounts = {};
for (let word of words) {
if (word.length > 0) {
localCounts[word] = (localCounts[word] || 0) + 1;
}
}
// Emit aggregated counts per mapper — this is the combiner logic inline
for (let [word, count] of Object.entries(localCounts)) {
emit(word, count);
}
}
// Without combiner: each word occurrence emits a separate (word,1) pair.
// With combiner: only one (word, N) pair per unique word per mapper.
// Network traffic drops from O(total words) to O(unique words per split).
Output
the 45
and 32
of 28
...
⚠ Production Trap:
Combiner functions must be associative and commutative. If your reduce function is not (e.g., calculating median), don't use a combiner — it will give wrong results. I've seen this cause silent data corruption in a financial reporting pipeline.
thecodeforge.io
Mapreduce Batch Processing
The Shuffle and Sort: The Hidden Bottleneck
Between map and reduce lies the shuffle — the most expensive phase. The framework sorts all intermediate keys, groups them, and transfers them to the correct reducer. This is a distributed sort over the network. If your keys are skewed (e.g., one key has 90% of the data), one reducer gets hammered while others sit idle. The fix is a custom partitioner that distributes keys more evenly. For example, if you're processing user data and one user has 10M events, hash partitioning sends all 10M to one reducer. A custom partitioner could split that user's data across multiple reducers using a secondary key.
CustomPartitioner.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — SystemDesign tutorial
// Custom partitioner to handle skewed keys
classSkewAwarePartitionerextendsPartitioner<Text, IntWritable> {
@OverridepublicintgetPartition(Text key, IntWritable value, int numPartitions) {
String keyStr = key.toString();
// If key is a hot key (e.g., user 'abc123'), spread across partitions using a hash of the value
if (keyStr.equals("abc123")) {
// Use value to distribute — this assumes value is some sub-key
return (keyStr.hashCode() + value.get()) % numPartitions;
}
// Default hash partitioner
return (keyStr.hashCode() & Integer.MAX_VALUE) % numPartitions;
}
}
// Set in driver: job.setPartitionerClass(SkewAwarePartitioner.class);
Output
No direct output — this is a config change. Effect: reducer load is balanced, job finishes in 2 hours instead of 14.
💡Interview Gold:
The shuffle is the most common bottleneck in MapReduce. Interviewers love asking: 'How would you handle data skew?' Answer: custom partitioner, combiner, or salting keys with a random prefix.
The Reduce Phase: Aggregation and Final Output
The reducer receives an iterator over all values for a given key, sorted. It applies your reduce function and writes the output — typically to HDFS. The number of reducers is critical: too few and you get long tails; too many and you create thousands of tiny files (the 'small files problem') that kill HDFS performance. Rule of thumb: set reducers to 0.95 (nodes mapred.tasktracker.reduce.tasks.maximum) for balanced jobs. For CPU-heavy reduces, use 1.75 * that value to keep nodes busy while some reducers finish early.
ReducerConfig.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — SystemDesign tutorial
// Driver configuration for optimal reducer count
// Cluster: 10 nodes, each with 8 cores, 32GB RAM
// mapred.tasktracker.reduce.tasks.maximum = 4 (default)
int nodes = 10;
int slotsPerNode = 4; // reduce slots per node
int reducers = (int) (0.95 * nodes * slotsPerNode); // 38 reducers
// ForCPU-heavy reduces:
int reducersCpuHeavy = (int) (1.75 * nodes * slotsPerNode); // 70 reducers
job.setNumReduceTasks(reducers);
// Also set memory: mapreduce.reduce.memory.mb = 4096 (4GB per reducer)
// mapreduce.reduce.java.opts = "-Xmx3072m" (heap within container)
Output
No direct output. Job runs with 38 reducers, finishes in 30 minutes. With 1 reducer, it took 6 hours.
⚠ Never Do This:
Setting reducers to 0 (map-only job) is fine for filtering. But setting reducers to 1 for a global sort is a disaster — you lose all parallelism. Use TotalOrderPartitioner instead.
When MapReduce Breaks: Real Failure Modes
MapReduce assumes tasks are independent and idempotent. When they're not, you get subtle bugs. Example: a reducer that writes to an external database — if the task fails and is re-executed, you get duplicate writes. The fix is to make reducers idempotent (e.g., use upsert) or move side effects to a post-processing step. Another common failure: speculative execution causing duplicate output. If your reducer writes to a file with a fixed name, two speculative copies will overwrite each other. Always write to unique task-attempt directories and rename on success.
IdempotentReducer.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — SystemDesign tutorial
// Reducer that writes to a database — must be idempotent
function reduce(key, values) {
let total = 0;
for (let value of values) {
total += value;
}
// UseUPSERT to avoid duplicates on re-execution
db.execute("INSERT INTO word_counts (word, count) VALUES (?, ?) ON DUPLICATE KEY UPDATE count = ?",
[key, total, total]);
}
// Without idempotency: a failed reducer re-executes and inserts a second row.
// WithUPSERT: the count is overwritten correctly.
Output
No output — database is updated correctly even if task runs twice.
🔥The Classic Bug:
Writing to HDFS from a reducer: use MultipleOutputs to avoid filename collisions. Never hardcode filenames — use part-r-xxxxx naming that the framework provides.
Beyond MapReduce: Spark, Flink, and the Modern Batch World
MapReduce as an execution engine is largely obsolete — Spark and Flink are faster because they keep data in memory and avoid writing intermediate results to disk. But the programming model lives on. Spark's map and reduceByKey are direct descendants. The lessons from MapReduce — combiner, partitioner, data skew, speculative execution — apply directly to Spark. The difference is that Spark's DAG optimizer can pipeline multiple stages, reducing disk I/O. But the trade-off is memory pressure: if your data doesn't fit in memory, Spark spills to disk and can be slower than MapReduce.
SparkWordCount.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — SystemDesign tutorial
// Spark equivalent of MapReduce word count
from pyspark importSparkContext
sc = SparkContext("local", "WordCount")
text_file = sc.textFile("hdfs://logs/2024/*.gz")
counts = (text_file
.flatMap(lambda line: line.lower().split("\\W+"))
.filter(lambda word: len(word) > 0)
.map(lambda word: (word, 1))
.reduceByKey(lambda a, b: a + b)) # This is the reduce phase
counts.saveAsTextFile("hdfs://output/wordcount")
# Spark's reduceByKey is a combiner + reducer in one.
# It performs a map-side combine (like combiner) and a reduce-side aggregation.
Output
Part-00000, Part-00001, ... files with word counts.
💡Senior Shortcut:
If your Spark job is slow, look at the DAG visualization. If you see 'Shuffle Read' taking most of the time, you have a data skew problem — same fix as MapReduce: salting or custom partitioner.
When Not to Use MapReduce or Batch Processing
MapReduce is overkill for datasets under 100GB — the overhead of starting containers, scheduling tasks, and shuffling data outweighs the parallelism. Use a single machine with parallel processing (e.g., Python multiprocessing, GNU Parallel). Also, don't use batch processing for real-time needs. If you need sub-second latency, use a stream processor like Kafka Streams or Apache Flink. I've seen teams build a 5-minute batch pipeline for a dashboard that needed 1-second freshness — they wasted months on tuning when a simple streaming solution would have worked.
WhenNotToUse.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — SystemDesign tutorial
// For datasets < 100GB, use a single machine with parallel processing:
// Python example:
from multiprocessing importPool
def process_file(filename):
# process a single file
pass
with Pool(8) as p: # 8 cores
p.map(process_file, file_list)
// NoHadoop, no Spark, no cluster overhead.
// This runs in minutes, not hours.
Output
Processed files in 2 minutes.
⚠ Production Trap:
Don't use batch processing for operational workloads that need ACID transactions. Batch jobs are eventually consistent by design. If you need strong consistency, use a database with transactions.
thecodeforge.io
Mapreduce Batch Processing
● Production incidentPOST-MORTEMseverity: high
The 4GB Container That Kept Dying
Symptom
A daily batch job processing 500GB of clickstream data failed every night at 3am with 'Container killed by the ApplicationMaster'.
Assumption
The team assumed they needed more memory — they doubled container memory to 8GB.
Root cause
The job used a single reducer for a global sort. The reducer's heap couldn't hold all the keys, causing GC thrashing and container timeout. The real issue was the number of reducers set to 1, not memory.
Fix
Set mapreduce.job.reduces to 20 (based on cluster size: 10 nodes × 2 cores per node). Also added a combiner to pre-aggregate data on the map side.
Key lesson
One reducer is a bottleneck.
Always set reducers to at least the number of nodes in your cluster, and use a combiner to reduce shuffle data.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Job stuck at 99% for hours — one reducer is slow
→
Fix
1. Check the Application Master UI for task durations. 2. Identify the slow task. 3. Look at its logs for GC pauses or data skew. 4. If skew: implement custom partitioner. 5. If GC: increase heap with mapreduce.reduce.java.opts.
Symptom · 02
Container killed with 'Physical memory limit exceeded'
→
Fix
1. Check mapreduce.reduce.memory.mb (or map phase equivalent). 2. Increase it by 50%. 3. Also increase mapreduce.reduce.java.opts -Xmx to 75% of container memory. 4. If still failing, add a combiner to reduce data volume.
Symptom · 03
Output has duplicate records
→
Fix
1. Check if speculative execution is enabled (mapreduce.map.speculative). 2. If yes, ensure reducers are idempotent (use upsert or write to unique files). 3. Alternatively, disable speculative execution for idempotent-sensitive jobs.
★ MapReduce and Batch Processing Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Job fails with `ShuffleError: Exceeded MAX_FAILED_UNIQUE_FETCHES`−
Jobs that don't fit in memory (spills kill performance)
⚙ Quick Reference
7 commands from this guide
File
Command / Code
Purpose
WordCount.systemdesign
function map(line) {
Why MapReduce Exists
MapWithCombiner.systemdesign
function map(line) {
The Map Phase
CustomPartitioner.systemdesign
class SkewAwarePartitioner extends Partitioner {
The Shuffle and Sort
ReducerConfig.systemdesign
int nodes = 10;
The Reduce Phase
IdempotentReducer.systemdesign
function reduce(key, values) {
When MapReduce Breaks
SparkWordCount.systemdesign
from pyspark import SparkContext
Beyond MapReduce
WhenNotToUse.systemdesign
from multiprocessing import Pool
When Not to Use MapReduce or Batch Processing
Key takeaways
1
MapReduce is a programming model, not just a framework
understand the map-shuffle-reduce pattern and it applies to Spark, Flink, and even SQL GROUP BY.
2
The shuffle is always the bottleneck. Combiner, partitioner, and reducer count tuning are the three levers that fix 90% of performance problems.
3
Never use batch processing for real-time needs, and never use MapReduce for datasets under 100GB
the overhead isn't worth it.
4
Idempotency is non-negotiable. If your reducer writes to an external system, make it idempotent or you'll get duplicates on task re-execution.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How does MapReduce handle a node failure during the reduce phase?
Q02SENIOR
When would you choose MapReduce over Spark for a batch job?
Q03SENIOR
What happens when a reducer's input is too large to fit in memory during...
Q04JUNIOR
What is the difference between a combiner and a reducer?
Q05SENIOR
Your MapReduce job produces 10,000 small output files. What's the proble...
Q06SENIOR
Design a batch processing system that processes 10TB of log data daily, ...
Q01 of 06SENIOR
How does MapReduce handle a node failure during the reduce phase?
ANSWER
The ApplicationMaster detects the failure via heartbeat timeout. It re-schedules the failed reduce task on another node. The new reducer fetches its input from the map output files on HDFS (or from other nodes if not yet persisted). The job continues from where it left off — no need to re-run map tasks unless the map outputs were lost.
Q02 of 06SENIOR
When would you choose MapReduce over Spark for a batch job?
ANSWER
When the dataset is larger than available cluster memory and the job is a simple one-pass aggregation. MapReduce spills to disk by design, so it can handle datasets that don't fit in memory without performance degradation. Spark would spill to disk too, but with overhead from its DAG scheduler and lineage tracking. Also, if your cluster is already running Hadoop and you don't want to manage another framework, MapReduce is simpler.
Q03 of 06SENIOR
What happens when a reducer's input is too large to fit in memory during the merge phase?
ANSWER
The reducer performs a multi-pass merge: it reads a portion of the sorted segments, merges them, writes the result to a temporary file, and repeats. This is called 'external merge'. It's slower but prevents OOM. The config mapreduce.task.io.sort.factor controls how many segments are merged per pass. Increase it to reduce the number of passes.
Q04 of 06JUNIOR
What is the difference between a combiner and a reducer?
ANSWER
A combiner runs on the map side after the map function, before the shuffle. It reduces the data volume sent over the network. A reducer runs on the reduce side after the shuffle. The combiner must be associative and commutative, and its output type must match the reducer's input type. The reducer can be any function. In word count, the combiner sums counts per mapper, reducing network traffic.
Q05 of 06SENIOR
Your MapReduce job produces 10,000 small output files. What's the problem and how do you fix it?
ANSWER
The problem is the 'small files problem' — HDFS struggles with many small files because each file consumes namenode memory and metadata overhead. Fix: reduce the number of reducers (e.g., set to 10-20). Or use a custom output format that merges small files into larger ones. Alternatively, run a post-processing job to concatenate files.
Q06 of 06SENIOR
Design a batch processing system that processes 10TB of log data daily, with a requirement that the output must be available by 6am every day. The cluster has 20 nodes with 64GB RAM each. How would you choose between MapReduce and Spark?
ANSWER
Given 10TB and 20 nodes, each node has 64GB RAM, total cluster memory is 1.28TB. The data is 10TB, so it won't fit in memory. Spark would spill to disk, losing its advantage. MapReduce is designed for disk-based processing and would be more predictable. I'd choose MapReduce with a well-tuned combiner and partitioner. Set reducers to 38 (0.95202). Use compression (LZO) for intermediate data. The job should finish in 3-4 hours, leaving a buffer for retries.
01
How does MapReduce handle a node failure during the reduce phase?
SENIOR
02
When would you choose MapReduce over Spark for a batch job?
SENIOR
03
What happens when a reducer's input is too large to fit in memory during the merge phase?
SENIOR
04
What is the difference between a combiner and a reducer?
JUNIOR
05
Your MapReduce job produces 10,000 small output files. What's the problem and how do you fix it?
SENIOR
06
Design a batch processing system that processes 10TB of log data daily, with a requirement that the output must be available by 6am every day. The cluster has 20 nodes with 64GB RAM each. How would you choose between MapReduce and Spark?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
What is MapReduce in simple terms?
MapReduce is a way to process huge datasets by splitting the work across many computers. You write two functions: 'map' which processes each piece of data independently, and 'reduce' which combines results by key. The framework handles the messy parts like distributing data, handling failures, and collecting results.
Was this helpful?
02
What's the difference between MapReduce and Spark?
MapReduce writes intermediate results to disk between stages, while Spark keeps them in memory. This makes Spark faster for iterative algorithms and interactive queries. But MapReduce is more predictable for very large datasets that don't fit in memory. Choose MapReduce for simple, disk-based batch jobs; choose Spark for complex pipelines or when speed matters.
Was this helpful?
03
How do I handle data skew in MapReduce?
Data skew happens when one key has far more values than others. Fix it with a custom partitioner that distributes the hot key across multiple reducers (e.g., by salting the key with a random prefix). Also use a combiner to reduce data volume before the shuffle. In Spark, use repartition or salting.
Was this helpful?
04
Can MapReduce handle real-time data?
No. MapReduce is designed for batch processing — jobs take minutes to hours. For real-time (sub-second) processing, use stream processing frameworks like Apache Kafka Streams, Apache Flink, or Spark Streaming (micro-batch). MapReduce is for historical analysis, not live dashboards.