RLE replaces consecutive identical symbols with a (count, symbol) pair.
Encode and decode both run in O(n) time, O(r) space (r = number of runs).
Best-case compression: ~120x for fax lines with long white runs.
Worst-case expansion: 2x for alternating data like 'ABAB'.
Production insight: RLE is never used alone in modern compressors — it's a building block in bzip2 (BWT+MTF+RLE+Huffman).
Biggest mistake: applying RLE to random or already-compressed data — you'll expand the file, not shrink it.
✦ Definition~90s read
What is Run-Length Encoding?
Run-Length Encoding (RLE) is a lossless compression scheme that replaces sequences of identical data values (runs) with a single value and a count. It's the simplest form of compression — conceptually, you take 'AAAAA' and store it as '5A'. RLE works well on data with long repeated runs, like simple graphics, binary images, or fax transmissions, but collapses on noisy or high-entropy data where it can actually expand the file.
★
Run-Length Encoding is the simplest compression idea: instead of storing AAABBBBBCCCC, store 3A5B4C.
Its extreme simplicity means it's often the first compression algorithm taught, and it's still used as a building block in more complex formats.
In the fax world, RLE was the backbone of Group 3 and Group 4 compression standards (T.4 and T.6), encoding each scanline of a black-and-white document as alternating white and black runs. The problem: the original fax standard used a single byte for the run count, limiting runs to 255 pixels.
When a faxed document had a horizontal white space wider than 255 pixels — common in margins or between columns — the encoder had to split the run, inserting a zero-length black run as a separator. This '255-run limit' caused subtle bugs in early fax implementations: some decoders would misinterpret the split, producing a thin black line across the page, or worse, corrupting the entire scanline.
The fix came with extended run-length codes (make-up codes) in the T.4 standard, but the legacy of this limit haunted fax compatibility for decades.
RLE appears in BMP (4-bit and 8-bit modes), PCX, and TIFF as an optional compression method, but it's rarely the best choice. For photographic images, RLE performs poorly compared to LZW or deflate. In bzip2, RLE is used as a preprocessing step — it runs a Burrows-Wheeler transform, then applies move-to-front coding, and finally RLE on the zero-run counts.
This layered approach exploits RLE's strength on structured data while avoiding its weaknesses. When you hit edge cases — like a run of 256 identical bytes — you need escape codes or multi-byte counts, which is where the fax bug originated. Modern implementations handle this transparently, but the fax 255-run limit remains a cautionary tale about trusting simple compression with real-world data.
Plain-English First
Run-Length Encoding is the simplest compression idea: instead of storing AAABBBBBCCCC, store 3A5B4C. Consecutive runs of the same character are replaced by a count and the character. It works brilliantly on data with long runs (fax images, simple graphics) and fails badly on random data (English text) where it can make things larger.
Run-Length Encoding is the first compression algorithm most people encounter, and it teaches the fundamental tension in all compression: the algorithm's effectiveness depends entirely on the structure of the data. For a fax transmission of a printed page — mostly white pixels with sparse black text — RLE achieves 50:1 compression. For a natural photograph, RLE can make the file larger.
This data-dependency is not a bug; it is the nature of compression. Shannon's source coding theorem says you can only compress data if it contains redundancy. RLE exploits one specific type of redundancy: runs of identical symbols. Understanding when your data has this structure — and when it doesn't — is the practical skill.
Run-Length Encoding — The Compression That Fax Machines Trusted Too Much
Run-length encoding (RLE) is a lossless compression scheme that replaces consecutive identical values (runs) with a count and the value. For example, 'AAAAABBBCC' becomes '5A3B2C'. It's the simplest form of compression — O(n) time, O(1) extra space — and works well when data has long runs of repetition, like black-and-white fax images or simple bitmap graphics.
RLE's critical property is that it compresses in a single pass with no dictionary or entropy model. But it also has a hard limit baked into many implementations: runs are typically capped at 255 because the count is stored in a single byte. This means a run of 300 identical pixels must be split into two runs (255 + 45), adding overhead and creating a subtle bug surface when software assumes runs never exceed 255.
Use RLE when you need fast, predictable compression on data with long repeated sequences — think telemetry logs with many identical readings, or monochrome image transmission. It's not suitable for general text or binary data with high entropy. The real-world relevance? RLE is the foundation of fax protocol T.4 (Group 3), where the 255-run limit directly causes corrupted pages when a scanner produces runs longer than expected.
⚠ The 255-Run Ceiling Is Not Theoretical
Many RLE implementations silently truncate runs at 255, causing data loss. Always validate that your encoder handles runs longer than 255 by splitting them explicitly.
📊 Production Insight
Fax servers receiving pages with long horizontal white runs (e.g., a blank margin) would silently drop pixels beyond 255, producing a black stripe or shifted image.
The symptom: a single-pixel-wide vertical line at column 255 in the decoded image, or a complete desync of the rest of the row.
Rule of thumb: if your RLE encoder uses a single byte for the count, you must split any run longer than 254 into two runs — never assume the input will respect your limit.
🎯 Key Takeaway
RLE compresses runs of identical values into (count, value) pairs, achieving O(n) compression with minimal memory.
The 255-run limit is a real bug: a single-byte count cannot represent runs ≥256, causing silent data corruption in fax and image protocols.
Always split runs longer than 254 explicitly in your encoder, and validate that your decoder handles split runs correctly.
thecodeforge.io
Run Length Encoding
Basic RLE Implementation
Here's the standard RLE encoder and decoder for strings. The encoder scans left to right, counting consecutive identical characters. When the character changes, it appends the (count, char) tuple. The decoder uses multiplication to expand each tuple.
Notice the edge case handling: empty input returns an empty list, and the final run is appended after the loop. You'll see that the compression ratio varies wildly — the fax line (720 characters, mostly long runs) compresses 120x, while alternating 'ABAB' expands by a factor of 2.
In a real fax pipeline, the encoder must handle runs that exceed 255 when using single-byte counts.
Always flush the last run outside the loop — a classic off-by-one bug causes data corruption.
Rule: test with empty input, single character, alternating pattern, and long runs.
🎯 Key Takeaway
RLE encode is O(n), decode is O(compressed size).
Compression ratio depends entirely on average run length.
If avg run < 2, RLE will expand your data — choose a different algorithm.
Binary RLE and Escape Codes
For raw byte data, we can pack run-length pairs as [count][byte]. The count is a single byte (0–255), limiting run length to 255. To handle longer runs, you split them into multiple pairs. This format is used in early image formats like PCX and in some raw bitmap encodings.
The example shows a solid block of 100 bytes of 0xFF, followed by 200 of 0x00, then 50 of 0x80. The byte-level RLE compresses this from 350 bytes to just 6 — a 58x ratio. But if the data is random (short runs), each byte pair uses 2 bytes, so random data doubles in size.
binary_rle.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
defrle_encode_bytes(data: bytes) -> bytes:
"""Compact binary RLE: [count][byte] pairs, count in 1 byte (max 255)."""ifnot data:
return b''
result = bytearray()
i = 0while i < len(data):
byte = data[i]
count = 1while i + count < len(data) and data[i+count] == byte and count < 255:
count += 1
result.extend([count, byte])
i += count
returnbytes(result)
raw = bytes([255]*100 + [0]*200 + [128]*50)
compressed = rle_encode_bytes(raw)
print(f'Raw: {len(raw)} bytes → Compressed: {len(compressed)} bytes ({len(raw)/len(compressed):.1f}x)')
Output
Raw: 350 bytes → Compressed: 6 bytes (58.3x)
📊 Production Insight
Binary RLE with a fixed 1-byte count fails on runs longer than 255 — you'll corrupt data silently.
The same caution applies to any fixed-size field in a binary compression format.
Rule: always cap run length at field max and emit multiple pairs, or use a variable-length encoding.
🎯 Key Takeaway
Byte-level RLE is simple but fragile with long runs.
Split runs longer than 255 into (255, byte) chunks.
Decoder must merge adjacent identical symbols to reproduce original data.
thecodeforge.io
Run Length Encoding
RLE in Image Formats: BMP, PCX, and TIFF
RLE was widely adopted in early image formats. BMP supports 4-bit and 8-bit RLE encoding (BI_RLE4, BI_RLE8) for indexed color images. PCX used RLE per scanline: if the two high bits of a byte are set, the lower 6 bits are a repeat count, otherwise the byte is a literal. TIFF has an RLE option in its compression tag.
In practice, these formats are legacy. Modern versions of BMP rarely use RLE because photographs produce short runs. However, for pixel art, icons, or diagrams with large uniform areas, RLE remains effective.
Mental Model
Mental model: RLE in image formats is like drawing with a paint bucket
Think of RLE as telling the computer "paint this color for the next N pixels" instead of listing every pixel.
A solid blue sky in a photo: runs are long, RLE shrinks data.
A grass texture with alternating green shades: runs are short, RLE expands.
BMP RLE writes a series of commands: (repeat, color) or (literal count, literals...).
PCX RLE distinguishes between literal and run bytes by checking the top bits.
📊 Production Insight
A common trap is assuming BMP RLE works on 24-bit images — it only applies to 8-bit or 4-bit color depth. Applying RLE to a true-color BMP will either fail or silently expand the file.
Rule: check the BMP header's bits-per-pixel field before choosing RLE encoding.
🎯 Key Takeaway
BMP RLE is limited to paletted images (<=8bpp).
PCX RLE uses a 6-bit count in the high nibble — max 63 repeats per byte.
For modern uses, prefer PNG (DEFLATE) over RLE-based formats.
RLE as a Building Block in bzip2
The bzip2 compression pipeline uses RLE in two places: 1. Pre-RLE: a first pass that replaces runs of 4–255 identical bytes with a special marker and a count. This is applied to the raw input to reduce space before the Burrows-Wheeler Transform (BWT). 2. Post-MTF RLE: after BWT and Move-to-Front (MTF) coding, the output contains many zero runs. RLE encodes these zero runs (run-length encoding of zeros) before final Huffman coding.
Together with BWT (which groups similar symbols together) and MTF (which outputs small numbers for frequent symbols), RLE turns a structured but not yet compressed intermediate into a highly compressible sequence. This is why bzip2 can beat gzip on certain data (e.g., text files with repetitive content).
bzip2_pre_rle.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Simplified pre-RLE step from bzip2 (run-length encoding of repeated bytes)defbzip2_pre_rle(data: bytes) -> bytes:
result = bytearray()
i = 0while i < len(data):
byte = data[i]
count = 1while i + count < len(data) and data[i+count] == byte and count < 256:
count += 1if count >= 4:
# Emit special symbol: (byte repeated 4 times) + (count-4 as byte)
result.extend([byte]*4)
result.append(count - 4)
else:
result.extend([byte]*count)
i += count
returnbytes(result)
# Example: 'AAAAA' -> 'AAAA' + 0x01 (since 5-4=1)print(bzip2_pre_rle(b'AAAAABBCCCDDDD'))
Output
b'AAAA\x01BBCCCDDDD\x00' # DDDD (4) is represented as 4 D's + 0x00 (4-4=0)
📊 Production Insight
If you run bzip2 on already-compressed data (jpeg, zip), the BWT stage often becomes ineffective, and the RLE stage may actually expand the data. The result: a larger file than the original.
Rule: never compress already-compressed data. Always check file type headers before processing.
🎯 Key Takeaway
RLE in bzip2 exploits the zero-run structure after MTF — that's the critical insight.
Without BWT+MTF, RLE alone can't achieve high compression on text.
bzip2's RLE is specialized (zero-only) — not a general-purpose run encoder.
Performance Considerations and Edge Cases
RLE is O(n) in time and O(r) in space (r = number of runs). Decode is O(n) in the uncompressed size. The algorithm is trivial, but practical implementations must handle:
Empty input: return empty output.
Single character: the loop must still append the final run.
Maximum run length: if using fixed-size count field, cap and split.
Non-printable symbols: binary data must encode counts and bytes correctly, not confuse them with delimiters.
Large datasets: RLE is not a bottleneck — even on 1 GB files, a naive Python implementation takes a few seconds.
Memory wise: the encoder stores the list of pairs, which can be large if the number of runs is high (e.g., random data). For worst-case (every character differs), the compressed output is 2x the input size. That's a 100% overhead. Always check the ratio before committing to RLE as a delivery format.
📊 Production Insight
In a real email attachment filter, RLE was used to reduce scan times on images. But the naive implementation allocated a new list for every image, causing OOM on 300 MB random-like graphics. Fix: use a streaming encoder that writes to a file directly, or pre-check the entropy.
Rule: stream encoding avoids memory explosions on pathological inputs.
🎯 Key Takeaway
RLE is compute-cheap but memory-proportional to number of runs.
Worst-case memory is 2x input size (every char is a new run).
Always bound memory usage — use streaming or chunked processing for large inputs.
Should you use RLE?
IfData has long runs (avg run > 10)
→
UseRLE is excellent — expect 5x+ compression.
IfData is random or already compressed
→
UseRLE expands (up to 2x) — use Huffman, LZ77, or a modern codec.
IfYou need both compression and real-time performance
→
UseRLE decode is fast enough for video streams — combine with other methods.
IfYou must compress binary data with unknown max run length
→
UseUse variable-length counts (e.g., escape code) or cap at 255 and split.
Why Counting Runs Is Harder Than It Looks
Most tutorials hand you the iteration approach like it's the only option. It isn't. It's the beginner option. But you need to understand why it works before you start sticking counts between characters. The core idea is dead simple: walk the input, count consecutive identical characters, and emit the character plus its count. That's it. No recursion, no state machine, no magic.
The trap is assuming this works for every encoding scenario. It doesn't. Binary RLE (which we covered earlier) needs escape codes. Image formats use run-length pairs. But for plain text compression in memory, this is your workhorse. The runtime is O(n) because you visit each character exactly once. The space is O(n) for the output string, which in the worst case—no runs—doubles the input size. That's a feature, not a bug. If you're worried about worst-case blowup, you're using the wrong algorithm for your data.
TextRleEncoder.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
// io.thecodeforge — dsa tutorialpublicclassTextRleEncoder {
publicstaticStringencode(String raw) {
if (raw == null || raw.isEmpty()) return"";
StringBuilder compressed = newStringBuilder();
int index = 0;
while (index < raw.length()) {
char current = raw.charAt(index);
int count = 1;
while (index + 1 < raw.length() && raw.charAt(index + 1) == current) {
count++;
index++;
}
compressed.append(current);
compressed.append(count);
index++;
}
return compressed.toString();
}
publicstaticvoidmain(String[] args) {
String test = "aaaabbbccc";
System.out.println(encode(test)); // Output: a4b3c3
test = "abbbcdddd";
System.out.println(encode(test)); // Output: a1b3c1d4
}
}
Output
a4b3c3
a1b3c1d4
⚠ Production Trap:
Never assume the input is clean. A null pointer or empty string will crash naive implementations. Always guard at the entry point. Your production crash log will thank you.
🎯 Key Takeaway
Text RLE is O(n) time, O(n) space, and fails silently on unsorted input—only works on runs of the same character.
When Character Counts Get Ugly — Multi-Digit Runs
Your first implementation probably handles 'aaa' → 'a3' just fine. But what about 100 consecutive 'x' characters? Your naive code dumps 'x100' into the output. That's three characters for one run. The decoder has no idea if 'x100' means character 'x', count 100, or character 'x1', count 0, followed by '0'. This ambiguity is why real RLE decoders use either fixed-width counts or escape codes.
Most production implementations cheat by using a delimiter or limiting run length to 255 (which fits in one byte). If you're building something that talks to legacy systems, like fax machines or BMP decoders, you must respect their count formats. Multi-digit counts explode the output size. A run of 999 takes three bytes instead of two. In a world where compression ratio matters, that's a sin.
The fix? Either store counts as bytes (0-255) and split long runs, or use a special marker to indicate that the following number is a count. The latter approach is what Binary RLE with escape codes does. Pick one, document it, and stick to it. Ambiguity is a bug.
RleFixedWidthDecoder.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
// io.thecodeforge — dsa tutorialpublicclassRleFixedWidthDecoder {
// Assumes count is always a single digit (max run of 9)// Real systems would use byte countspublicstaticStringdecode(String encoded) {
if (encoded == null || encoded.length() < 2) return"";
StringBuilder original = newStringBuilder();
int i = 0;
while (i < encoded.length() - 1) {
char character = encoded.charAt(i);
int count = encoded.charAt(i + 1) - '0';
if (count < 1) {
thrownewIllegalArgumentException("Bad RLE count at index " + (i + 1));
}
original.append(String.valueOf(character).repeat(count));
i += 2;
}
return original.toString();
}
publicstaticvoidmain(String[] args) {
String input = "a4b3c3";
System.out.println(decode(input)); // Output: aaaabbbccc
}
}
Output
aaaabbbccc
💡Senior Shortcut:
For text protocols, limit run length to 9 and use a single digit count. For binary, use a byte (0-255) and split runs longer than 255. Keeps decoding simple and safe.
🎯 Key Takeaway
Multi-digit counts break simple decoders — always define a fixed width or use escape codes.
● Production incidentPOST-MORTEMseverity: high
The 255-Run Limit That Corrupted a Fax Archive
Symptom
Fax images showed horizontal white bands at regular intervals, exactly 256 pixels wide. The same pattern appeared on every page with solid white margins.
Assumption
The RLE implementation was correct because it passed unit tests with short runs. The team assumed fax pages never have runs longer than 255 bytes.
Root cause
RLE encoder used byte-sized count field (max 255). Fax pages with margins of 300+ white pixels caused the encoder to split the run into two (255, then remainder). The decoder treated those as separate runs and printed them sequentially, creating a visible seam on every row.
Fix
Changed the encoder to use a two-byte count (uint16) or to emit multiple (255, symbol) pairs and have the decoder merge adjacent identical runs. The second approach avoided breaking existing decoders.
Key lesson
Never assume maximum run length fits in a single byte — real-world data always surprises you.
Always test edge cases: empty input, single character, maximum runs, alternating patterns.
Document the count field size in the format spec — every implementer needs to know.
Production debug guideSymptom → Action guide for common RLE failures in production4 entries
Symptom · 01
Decoded output does not match the original input
→
Fix
Verify run count values: print all (count, symbol) pairs and compare to the original data. Look for off-by-one errors on the last run or incorrect handling of single-character input.
Symptom · 02
Compression ratio is worse than expected (below 1.0)
→
Fix
Check if the input data is random or already compressed. RLE expands on data with short runs. If you can't change the data, consider using a different compressor (LZ77, Huffman).
Symptom · 03
Encoded output has incorrect length or trailing bytes
→
Fix
Inspect the encoding loop: ensure you flush the final run after the last character. Use a debug print to confirm the loop iterates over all characters including the last.
Symptom · 04
Binary RLE produces invalid bytes (count > 255)
→
Fix
Enforce count cap at 255 and emit multiple pairs for longer runs. Or switch to a variable-length encoding (e.g., use 0x00 as an escape for large counts).
★ RLE Debug Cheat SheetQuick diagnostic commands and fixes for common RLE problems
What type of data does RLE compress well, and what data does it expand?
Q02JUNIOR
Implement RLE encode and decode for a string.
Q03SENIOR
How is RLE used in the bzip2 compression pipeline?
Q04SENIOR
What is the worst-case expansion ratio for RLE?
Q01 of 04JUNIOR
What type of data does RLE compress well, and what data does it expand?
ANSWER
RLE compresses well on data with long runs of identical symbols: fax images (mostly white), simple graphics, pixel art. It expands on data with short runs: natural language text, random data, already-compressed files. The compression ratio equals the inverse of the average run length (theoretical limit).
Q02 of 04JUNIOR
Implement RLE encode and decode for a string.
ANSWER
``python
def rle_encode(s):
if not s: return []
res, count = [], 1
for i in range(1, len(s)):
if s[i] == s[i-1]: count += 1
else: res.append((count, s[i-1])); count = 1
res.append((count, s[-1]))
return res
def rle_decode(enc):
return ''.join(ch*c for c,ch in enc)
``
Edge cases: empty string, single character, alternating patterns.
Q03 of 04SENIOR
How is RLE used in the bzip2 compression pipeline?
ANSWER
bzip2 applies RLE twice: first on the raw input (run-length encoding of identical bytes, runs >=4), and second after the Burrows-Wheeler Transform and Move-to-Front coding (encoding zero runs). The zero-run RLE exploits the large number of zeros produced by MTF. This is essential because BWT produces runs of repeated characters, and MTF maps them to zeros, so runs of zeros are long and compressible with RLE.
Q04 of 04SENIOR
What is the worst-case expansion ratio for RLE?
ANSWER
The worst case is every character differs from its predecessor (e.g., alternating 'ABABAB...'). For a string of length n, RLE produces n pairs, each taking 2 units (count + symbol). So compressed size = 2n, original = n → expansion ratio 2.0. In practice, if the count field is larger than 1 byte or uses variable-length encoding, the expansion can be worse. The theoretical worst case for any lossless compressor is unbounded if the data is incompressible, but RLE's worst-case output size is O(input_size).
01
What type of data does RLE compress well, and what data does it expand?
JUNIOR
02
Implement RLE encode and decode for a string.
JUNIOR
03
How is RLE used in the bzip2 compression pipeline?
SENIOR
04
What is the worst-case expansion ratio for RLE?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Does JPEG use RLE?
JPEG uses RLE for zero-coefficient runs in its zig-zag scan step (encoding runs of zeros after quantisation), combined with Huffman coding. It is not pure RLE — the DCT and quantisation stages do the heavy lifting.
Was this helpful?
02
Is RLE used in modern formats like PNG?
PNG uses DEFLATE (LZ77 + Huffman), not standalone RLE. However, some webP and lossless JPEG variants include RLE-like steps. For modern lossless compression, RLE appears only as one part of a pipeline, never alone.
Was this helpful?
03
Can RLE compress encrypted data?
No. Encrypted data is indistinguishable from random data. RLE will expand it by roughly 2x. Encrypted data must be compressed before encryption, not after.
Was this helpful?
04
What is the difference between RLE and LZ77?
RLE only encodes runs of identical symbols. LZ77 encodes arbitrary repeated sequences (not necessarily repeated characters). RLE is a special case of LZ77 where the match is always length N of the same symbol. LZ77 is more general and achieves higher compression on text.