Skip to content
StackPractices
intermediate By Mathias Paulenko

Read Large Files

How to read large files efficiently without running out of memory.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Reading multi-gigabyte files into memory at once can crash applications or cause severe performance degradation. The following demonstrates memory-efficient techniques to process large files line by line or in chunks across Python, JavaScript, and Java.

When to Use

Use this resource when:

  • Processing log files, CSV dumps, or datasets larger than available RAM
  • Building ETL pipelines that ingest massive files
  • Streaming file contents to avoid blocking the event loop or heap

Solution

Python

# Line-by-line streaming (memory-efficient)
with open('large-file.log', 'r', encoding='utf-8') as f:
    for line in f:
        process(line)

# Chunked binary reading
chunk_size = 1024 * 1024  # 1 MB
with open('large-file.bin', 'rb') as f:
    while chunk := f.read(chunk_size):
        process(chunk)

JavaScript

const fs = require('fs');
const readline = require('readline');

// Line-by-line with readline (Node.js)
const stream = fs.createReadStream('large-file.log');
const rl = readline.createInterface({ input: stream });

for await (const line of rl) {
    console.log(line);
}

// Chunked reading
const readable = fs.createReadStream('large-file.bin', { highWaterMark: 1024 * 1024 });
readable.on('data', chunk => process(chunk));

Java

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class LargeFileReader {
    // Line-by-line
    public void readLines(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            String line;
            while ((line = reader.readLine()) != null) {
                process(line);
            }
        }
    }

    // Memory-mapped chunk reading
    public void readChunks(String path) throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
            while (channel.read(buffer) > 0) {
                buffer.flip();
                process(buffer);
                buffer.clear();
            }
        }
    }

    private void process(Object data) {}
}

Explanation

Line-by-line streaming keeps only one line in memory at a time, making it ideal for text logs and CSV files. Chunked reading processes fixed-size byte blocks, suitable for binary data or when you need to control buffer size precisely. Memory-mapped files (Java) let the OS handle paging directly, often faster for random access but uses virtual address space.

Variants

TechnologyApproachNotes
Pythonmmap moduleMaps file to memory; OS handles paging
JavaScriptWeb Streams APIReadableStream.getReader() in browsers
JavaFiles.lines()Lazy Stream; auto-closes

What Works

  1. Always use with (Python), try-with-resources (Java), or pipe error handling (JS) to prevent file descriptor leaks
  2. Choose buffer sizes based on average line length; 1 MB is a sane default
  3. For CSV/JSONL, parse incrementally rather than loading the entire structure
  4. Monitor memory usage with OS tools to verify streaming behavior
  5. Use mmap when you need random access without loading the whole file

Common Mistakes

  1. Calling read() or readFileSync() on large files loads everything into RAM
  2. Forgetting to handle encoding errors, which crash streams mid-file
  3. Using too small a chunk size, causing excessive system call overhead
  4. Not closing file handles, leading to “too many open files” errors
  5. Ignoring backpressure when piping to slow consumers

FAQ

How large is “large”?

Any file approaching or exceeding your process heap/RAM (e.g., >500 MB on a 2 GB container). Streaming is cheap; always prefer it for files over a few megabytes.

Does line-by-line work for binary files?

No. Binary files should use chunked byte reading. Line-based approaches assume newline delimiters and text encoding.

Is memory-mapped faster than streaming?

For sequential access, usually not dramatically. Memory mapping shines for random access patterns or when multiple processes share the same file.

Advanced Solutions

Python: Generators for lazy processing with progress tracking

import os
from pathlib import Path
from typing import Iterator

def read_lines_lazy(path: str, encoding: str = 'utf-8') -> Iterator[str]:
    """Yield lines one at a time. Memory usage is O(1) regardless of file size."""
    with open(path, 'r', encoding=encoding) as f:
        for line in f:
            yield line.rstrip('\n\r')

def read_chunks(path: str, chunk_size: int = 1024 * 1024) -> Iterator[bytes]:
    """Yield binary chunks. Memory usage is O(chunk_size)."""
    with open(path, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

def process_with_progress(path: str, process_fn, encoding: str = 'utf-8') -> int:
    """Process file line by line with progress tracking. Returns line count."""
    file_size = os.path.getsize(path)
    bytes_read = 0
    line_count = 0

    with open(path, 'r', encoding=encoding) as f:
        for line in f:
            process_fn(line.rstrip('\n\r'))
            bytes_read += len(line.encode(encoding))
            line_count += 1
            if line_count % 10000 == 0:
                pct = (bytes_read / file_size) * 100 if file_size > 0 else 0
                print(f"Progress: {pct:.1f}% ({line_count} lines)")
    return line_count

def read_csv_streaming(path: str, encoding: str = 'utf-8'):
    """Stream CSV rows without loading entire file. Returns iterator of dicts."""
    import csv
    with open(path, 'r', encoding=encoding, newline='') as f:
        reader = csv.DictReader(f)
        for row in reader:
            yield row

def read_jsonl_streaming(path: str, encoding: str = 'utf-8'):
    """Stream JSONL (one JSON object per line) without loading entire file."""
    import json
    for line in read_lines_lazy(path, encoding):
        if line.strip():
            yield json.loads(line)

# Usage
# for line in read_lines_lazy('10gb.log'):
#     if 'ERROR' in line:
#         print(line)
# count = process_with_progress('large.log', lambda l: None)
# for row in read_csv_streaming('data.csv'):
#     process_row(row)

Node.js: Streaming with backpressure and error recovery

const fs = require('fs');
const readline = require('readline');
const { pipeline } = require('stream');
const { promisify } = require('util');
const pipe = promisify(pipeline);

async function readLinesProcess(path, processFn) {
    const stream = fs.createReadStream(path, { encoding: 'utf-8' });
    const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });

    let count = 0;
    for await (const line of rl) {
        await processFn(line);
        count++;
    }
    return count;
}

async function readChunksProcess(path, processFn, chunkSize = 1024 * 1024) {
    const stream = fs.createReadStream(path, { highWaterMark: chunkSize });
    let totalBytes = 0;

    for await (const chunk of stream) {
        await processFn(chunk);
        totalBytes += chunk.length;
    }
    return totalBytes;
}

async function streamToTransform(srcPath, destPath, transformFn) {
    const src = fs.createReadStream(srcPath, { encoding: 'utf-8' });
    const dest = fs.createWriteStream(destPath, { encoding: 'utf-8' });

    const { Transform } = require('stream');
    let lineBuffer = '';

    const transform = new Transform({
        transform(chunk, encoding, callback) {
            lineBuffer += chunk;
            const lines = lineBuffer.split('\n');
            lineBuffer = lines.pop();
            for (const line of lines) {
                const result = transformFn(line);
                if (result !== null) this.push(result + '\n');
            }
            callback();
        },
        flush(callback) {
            if (lineBuffer) {
                const result = transformFn(lineBuffer);
                if (result !== null) this.push(result + '\n');
            }
            callback();
        },
    });

    await pipe(src, transform, dest);
}

// Usage
// const count = await readLinesProcess('large.log', async (line) => {
//     if (line.includes('ERROR')) console.error(line);
// });
// console.log(`Processed ${count} lines`);
// await streamToTransform('input.log', 'output.log', line => line.toUpperCase());

Java: NIO memory-mapped files and parallel line processing

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;

public class AdvancedFileReader {

    // Memory-mapped file reading for random access
    public static void readMapped(String path, long chunkSize) throws IOException {
        try (FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ)) {
            long fileSize = channel.size();
            long position = 0;
            while (position < fileSize) {
                long remaining = fileSize - position;
                long mapSize = Math.min(chunkSize, remaining);
                MappedByteBuffer buffer = channel.map(
                    FileChannel.MapMode.READ_ONLY, position, mapSize
                );
                processBuffer(buffer);
                position += mapSize;
            }
        }
    }

    private static void processBuffer(MappedByteBuffer buffer) {
        buffer.load();
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            // Process byte
        }
    }

    // Parallel line processing with Files.lines()
    public static long processLinesParallel(String path,
                                            java.util.function.Consumer<String> processor)
            throws IOException {
        try (Stream<String> lines = Files.lines(Paths.get(path), StandardCharsets.UTF_8)) {
            return lines.parallel()
                .peek(processor)
                .count();
        }
    }

    // Buffered reading with configurable buffer size
    public static int readBuffered(String path, int bufferSize,
                                   java.util.function.Consumer<String> lineHandler)
            throws IOException {
        int count = 0;
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    new FileInputStream(path), StandardCharsets.UTF_8), bufferSize)) {
            String line;
            while ((line = reader.readLine()) != null) {
                lineHandler.accept(line);
                count++;
            }
        }
        return count;
    }

    // Read file in chunks using NIO
    public static long readChunksNIO(String path, int chunkSize) throws IOException {
        long totalBytes = 0;
        try (SeekableByteChannel channel = Files.newByteChannel(
                Paths.get(path), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(chunkSize);
            while (channel.read(buffer) > 0) {
                buffer.flip();
                totalBytes += buffer.remaining();
                buffer.clear();
            }
        }
        return totalBytes;
    }
}

// Usage
// AdvancedFileReader.readMapped("large.bin", 1024 * 1024 * 100); // 100MB chunks
// long count = AdvancedFileReader.processLinesParallel("large.log",
//     line -> { if (line.contains("ERROR")) System.err.println(line); });
// int lines = AdvancedFileReader.readBuffered("data.csv", 65536, System.out::println);

Bash: Efficient file reading with while-read and awk

#!/usr/bin/env bash
set -euo pipefail

# Read line by line (memory-efficient, handles special chars)
read_lines() {
    local file="$1"
    while IFS= read -r line || [[ -n "$line" ]]; do
        echo "$line"
    done < "$file"
}

# Process with awk (fastest for text processing)
process_with_awk() {
    local file="$1"
    local pattern="${2:-ERROR}"
    awk -v pat="$pattern" '$0 ~ pat { count++ } END { print count " matches" }' "$file"
}

# Read in chunks using dd (for binary files)
read_chunks_dd() {
    local file="$1"
    local chunk_size="${2:-1048576}"  # 1MB default
    local offset=0
    local file_size
    file_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
    while (( offset < file_size )); do
        dd if="$file" bs="$chunk_size" skip=$((offset / chunk_size)) count=1 2>/dev/null
        offset=$((offset + chunk_size))
    done
}

# Count lines efficiently (wc -l is faster than while-read)
count_lines() {
    local file="$1"
    wc -l < "$file" | tr -d ' '
}

# Filter and count in one pass (avoid multiple reads)
filter_and_count() {
    local file="$1"
    local pattern="$2"
    grep -c "$pattern" "$file"
}

# Read specific line range (sed for small ranges, tail+head for large)
read_line_range() {
    local file="$1"
    local start="$2"
    local end="$3"
    sed -n "${start},${end}p" "$file"
}

# Usage
# read_lines large.log | head -100
# process_with_awk large.log ERROR
# count_lines large.log
# read_line_range large.log 1000 1010

Additional Best Practices

  1. Use csv.DictReader for CSV files instead of manual parsing. It handles quoted fields, embedded commas, and multi-line records correctly:
import csv

def process_csv(path: str) -> int:
    """Stream CSV rows as dicts. Handles quoted fields and embedded newlines."""
    count = 0
    with open(path, 'r', encoding='utf-8', newline='') as f:
        reader = csv.DictReader(f)
        for row in reader:
            process_row(row)
            count += 1
    return count

# process_csv('10gb_export.csv')
  1. Set highWaterMark explicitly in Node.js streams. The default is 16KB which causes excessive system calls. For large files, use 256KB-1MB:
const fs = require('fs');

// Good: explicit highWaterMark reduces syscall overhead
const stream = fs.createReadStream('large.log', {
    highWaterMark: 1024 * 1024,  // 1MB
    encoding: 'utf-8',
});

// Default: 16KB causes many small reads
// const stream = fs.createReadStream('large.log');
  1. Use Files.lines() with try-with-resources in Java. It returns a lazy Stream<String> that reads lines on demand. The stream must be closed to release the file handle:
import java.nio.file.*;
import java.util.stream.Stream;

// Good: try-with-resources ensures the file handle is closed
try (Stream<String> lines = Files.lines(Path.of("large.log"))) {
    lines.filter(l -> l.contains("ERROR"))
         .forEach(System.out::println);
}

// Bad: stream not closed, file descriptor leaks
// Stream<String> lines = Files.lines(Path.of("large.log"));
// lines.filter(l -> l.contains("ERROR")).forEach(System.out::println);
// // file handle never closed

Additional Common Mistakes

  1. Using readlines() for large files in Python. readlines() loads all lines into a list. For files larger than a few MB, iterate directly:
# Bad: loads entire file into memory as a list
with open('large.log') as f:
    lines = f.readlines()
    for line in lines:
        process(line)

# Good: iterates lazily, one line at a time
with open('large.log') as f:
    for line in f:
        process(line)
  1. Not handling stream errors in Node.js. Unhandled 'error' events crash the process. Always attach error handlers or use pipeline():
const fs = require('fs');
const { pipeline } = require('stream');
const { promisify } = require('util');

// Bad: no error handling, crashes on file not found
// const stream = fs.createReadStream('missing.log');
// stream.on('data', chunk => process(chunk));

// Good: pipeline handles errors and cleanup
async function safeRead(path) {
    const stream = fs.createReadStream(path);
    const { Writable } = require('stream');
    const sink = new Writable({
        write(chunk, encoding, callback) {
            process(chunk);
            callback();
        },
    });
    await promisify(pipeline)(stream, sink);
}
  1. Using Files.readAllLines() for large files in Java. readAllLines() loads all lines into a List<String>. Use Files.lines() for lazy streaming:
import java.nio.file.*;
import java.util.List;

// Bad: loads entire file into memory
// List<String> lines = Files.readAllLines(Path.of("10gb.log"));

// Good: lazy stream, one line at a time
try (Stream<String> lines = Files.lines(Path.of("10gb.log"))) {
    lines.forEach(line -> process(line));
}

Additional FAQ

How do I read specific line ranges without loading the whole file?

In Python, use itertools.islice with a generator. In Bash, use sed -n 'start,end p'. In Java, use Files.lines().skip(n).limit(m):

from itertools import islice

def read_line_range(path: str, start: int, count: int) -> list[str]:
    """Read `count` lines starting from line `start` (0-indexed)."""
    with open(path, 'r', encoding='utf-8') as f:
        return list(islice(f, start, start + count))

# lines = read_line_range('large.log', 1000, 10)  # Lines 1000-1009
# Bash: read lines 1000-1010
sed -n '1000,1010p' large.log

# Faster for large offsets: skip with tail
tail -n +1000 large.log | head -n 10

Is this solution production-ready?

Yes. Python’s file iteration protocol is used by the standard library, Django’s file upload handlers, and pandas’ chunked CSV reader. Node.js readline and createReadStream with pipeline() are used by Express.js, Next.js, and the AWS SDK for S3 downloads. Java’s BufferedReader and Files.lines() are used by Spring Batch, Apache Spark for text file ingestion, and Elasticsearch for log file parsing. Bash while read is the standard for log processing in logrotate, fail2ban, and systemd journal processing. The memory-mapped file pattern is used by MongoDB’s storage engine (WiredTiger), Lucene for index segments, and Kafka for log segment reads.

What are the performance characteristics?

Python for line in f processes 500K-1M lines/s with ~0.01ms per line overhead. Node.js readline processes 300K-800K lines/s; createReadStream with 1MB highWaterMark processes 50-200MB/s. Java BufferedReader.readLine() processes 500K-1.2M lines/s with default 8KB buffer; increasing to 64KB buffer improves throughput by 20-40%. Java Files.lines().parallel() scales linearly with cores for CPU-bound processing but adds 5-10ms overhead for stream splitting. MappedByteBuffer reads at 500-2000MB/s for sequential access, limited by disk I/O. Bash while read processes 50K-200K lines/s due to subshell overhead per line; awk processes 500K-2M lines/s. grep processes 200-800MB/s for simple patterns. Memory usage: line-by-line reading uses O(max_line_length) per line. Chunked reading uses O(chunk_size). Memory-mapped reading uses O(map_size) of virtual address space but physical memory is managed by the OS page cache.

How do I debug issues with this approach?

For memory issues, monitor with ps aux | grep <pid> (RSS column) or top -p <pid> — RSS should stay flat during streaming. For slow reads, check disk I/O with iostat -x 1 (Linux) or Activity Monitor > Disk (macOS). For encoding errors, inspect bytes with xxd file.txt | head or hexdump -C file.txt | head to detect BOM or mixed encodings. For file descriptor leaks in Python, check len(psutil.Process().open_files()) or lsof -p <pid> | wc -l. In Node.js, check process._getActiveHandles() for unclosed streams. In Java, check ManagementFactory.getPlatformMBeanServer() for java.nio:type=BufferPool,name=direct to monitor direct buffer usage. For backpressure issues in Node.js, look for 'drain' event warnings or use stream.writableNeedDrain. For slow Bash while read, replace with awk or grep which are 10-50x faster for text processing. For memory-mapped file issues in Java, check ulimit -v for virtual memory limits and ensure mapSize does not exceed available address space.