read-last-lines

Read a file's last N lines in Node.js without scanning the whole file, using efficient reverse chunked reads.

Library
npm
v1.10.0
55stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
64/100Good
Development Activity76
Maintenance64
Community44
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
75/100Good
Architecture74
Code Quality82
Innovation55
Learning Curve90

read-last-lines is a small, focused Node.js utility for grabbing the tail end of a file — the last N lines — without loading or scanning the entire file into memory. It reads the file backward in fixed-size chunks from the end, counting newline boundaries as it goes, and stops as soon as it has collected enough lines. This makes it well suited for tailing large log files, previewing the end of large text output, or building simple log-viewer tooling where reading gigabytes of file just to get the last few lines would be wasteful.

The API is a single async function, read(path, maxLineCount, encoding), returning a promise that resolves to either a string (default utf8, or any Node BufferEncoding) or a raw Buffer when 'buffer' is passed as the encoding. It correctly handles edge cases that trip up naive tail implementations: missing trailing newlines, Windows-style CRLF line endings, multi-byte UTF-8 characters split across chunk boundaries, empty files, and files made up entirely of blank lines. It is used inside larger projects such as Signal Desktop and Ghost CLI for exactly this kind of tail-reading behavior.

What You Get

  • A single read(path, maxLineCount, encoding) async function with a minimal, predictable API
  • Reverse chunked reads (64KB chunks) so only the tail of a file is ever touched, regardless of overall file size
  • String output in any Node BufferEncoding (default utf8), or raw Buffer output when 'buffer' is passed as the encoding
  • TypeScript type definitions (src/index.d.ts) included in the package, with overloads for the buffer vs. string return types
  • Correct handling of missing trailing newlines, Windows CRLF line endings, and multi-byte UTF-8 sequences split across chunk boundaries
  • Graceful behavior on edge cases: empty files, all-blank-line files, maxLineCount of 0 or negative, and non-existent files (rejects with a clear "file does not exist" error)

Common Use Cases

  • Tailing the last N lines of an application or server log file for a status page or CLI diagnostic
  • Building a lightweight log-viewer or tail -f-style feature without shelling out to a system tail binary
  • Previewing the end of large generated text files (exports, dumps, reports) in a web UI without downloading the whole file
  • Reading the last few entries of an append-only file (audit trail, changelog, event log) for a quick summary

Under The Hood

Architecture The library is a single exported async function in src/index.js with no internal module boundaries: read() opens a file handle via fs/promises, reads its size via stat(), then walks backward through the file in fixed 64KB chunks using explicit position reads on the same handle, scanning each chunk’s bytes in reverse for newline (0x0a) boundaries and stopping as soon as maxLineCount lines have been counted. Two small helpers, logicalLineCount and finalizeLines, clean up edge cases afterward — trimming an accidental leading newline left over from a chunk boundary and discarding any extra lines the chunked scan overshot. There is no dependency injection or plugin surface; the whole module is self-contained, so the only thing that could break by changing the core abstraction is the byte-scanning loop itself.

Tech Stack The runtime has zero production dependencies, relying solely on Node’s built-in fs/promises module (requiring Node >=20 per engines). Development tooling includes Mocha and Chai/chai-as-promised for behavioral tests, tsd for type-level testing of the bundled .d.ts file, ESLint (eslint:recommended, tab indentation, double quotes) for linting, and benchmark/benny plus a custom harness under analyze/ for comparing this implementation’s performance against alternate tail-reading strategies (legacy bytewise, chunked reverse, split-slice, and the system tail binary).

Code Quality The test suite in test/index.spec.js is thorough for a library this size, covering trailing-newline handling, Windows CRLF line endings, multi-byte UTF-8 content split across chunk boundaries, buffer vs. string output, unreadable files (EACCES/EPERM), empty files, newline-only files, non-existent files, and invalid encodings. Error handling is explicit: a try/catch/finally wraps the read loop, file handles are always closed, and ENOENT is normalized into a descriptive "file does not exist" error rather than leaking the raw Node error code. GitHub Actions workflows run tests, linting, and CodeQL analysis on every change, giving the project real CI coverage despite its small surface area.

API Design The public API is intentionally minimal — one function, three parameters, sensible defaults (utf8 encoding) — which keeps the learning curve close to zero for new consumers. The 'buffer' encoding escape hatch and bundled TypeScript overloads (returning Promise<Buffer> vs Promise<string> depending on the encoding argument) are a thoughtful touch that most single-purpose utility packages skip. The underlying reverse-chunk-scan technique is a well-known tail-reading pattern rather than a novel algorithm, and the project’s own benchmark notes acknowledge that alternate strategies (chunked-reverse, split-slice) can outperform the current implementation on some file-size profiles.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search