byte-counter
byte-counter is a tiny pass-through transform that totals the bytes crossing a stream without changing the chunks. The default export wraps the standard Web Streams API for fetch responses and browser-style pipelines; byte-counter/node provides a Node.js Transform for files, HTTP bodies, and pipeline(). A named byteLength helper also measures UTF-8 strings and binary views without constructing a stream. It reports transferred bytes, not characters, records, formatted sizes, rates, or percentages.
A focused, dependency-free choice when an ESM application on Node 20+ needs a passive byte total across Web or Node streams. Choose a progress or limiting transform if the count must drive policy rather than simple observation.
Use it if
- You need the exact byte total for a Web Streams pipeline while preserving its chunks and backpressure
- You need a Node Transform that can sit between an existing readable and writable without changing either endpoint
- You want one ESM package with typed entry points for both Web Streams and Node streams
- You need to compare bytes actually processed with an expected size after a transfer completes
- Your application runs on Node 18 or older: version 0.1.0 declares Node 20 or newer and offers no compatibility build
- Your project is CommonJS: the package declares type module and exports no require condition, so require('byte-counter') is not a supported entry point
- You need progress percentages, rate smoothing, elapsed time, or ETA: the only state exposed by the stream is a cumulative count
- You need to enforce a hard byte limit: the transform observes bytes but never aborts or rejects when a threshold is crossed
- You process object-mode streams: the source returns zero for values outside strings, ArrayBuffers, SharedArrayBuffers, and ArrayBuffer views, so objects pass through without contributing to the count
Setup reality
Installation is only npm install byte-counter, with no runtime dependencies, native compilation, credentials, or configuration file. The compatibility decisions matter more than the installation. Version 0.1.0 is ESM-only and declares Node 20 or newer. Import the default byte-counter entry for Web Streams, but import byte-counter/node for Node's Transform API; using the wrong one produces an object with the wrong piping surface. The count is read-only and starts at zero, then accumulates for the lifetime of that counter, so create a fresh instance for each independent transfer and read the final value only after the pipeline has finished. The transform preserves chunks and normal stream backpressure, but it does not emit progress events, calculate a percentage, stop oversized input, reset itself, or compare against Content-Length. Strings are measured as UTF-8 by TextEncoder, while Uint8Array, Buffer, DataView, typed arrays, ArrayBuffer, and SharedArrayBuffer use byteLength. Unsupported runtime values quietly contribute zero in the JavaScript implementation, even though the TypeScript signature rejects them. Fetch adds another trap: Content-Length can describe an encoded response while response.body exposes decoded bytes, so equality is meaningful only when transfer encoding and content encoding are understood. The counter uses a JavaScript number, which is plenty for ordinary transfers but is not a bigint accounting primitive for totals beyond the safe-integer range.
Patterns
Measure a UTF-8 stringmeasure-utf8-string
import {byteLength} from 'byte-counter';
console.log(byteLength('Hello 👋'));
// 10This counts UTF-8 bytes, not JavaScript UTF-16 code units or visible characters; the waving-hand emoji contributes four bytes.
Measure a binary viewmeasure-binary-view
import {byteLength} from 'byte-counter';
const packet = new Uint8Array([0, 127, 128, 255]);
console.log(byteLength(packet));
// 4Buffer, DataView, typed arrays, ArrayBuffer, and SharedArrayBuffer are also accepted because the implementation reads their byteLength.
Count a fetched response bodycount-fetch-body
import ByteCounterStream from 'byte-counter';
const response = await fetch('https://example.com/file.zip');
if (!response.ok || !response.body) {
throw new Error(`Download failed: ${response.status}`);
}
const counter = new ByteCounterStream();
await response.body
.pipeThrough(counter)
.pipeTo(new WritableStream({
write(chunk) {
consume(chunk);
},
}));
console.log(counter.count);Check response.body because it can be null, and read the final count only after pipeTo resolves.
Compare a completed fetch with Content-Lengthvalidate-content-length
const expected = Number(response.headers.get('content-length'));
const counter = new ByteCounterStream();
await response.body.pipeThrough(counter).pipeTo(destination);
if (Number.isFinite(expected) && counter.count !== expected) {
throw new Error(`Expected ${expected} bytes, received ${counter.count}`);
}Do not make this comparison blindly for compressed responses: Content-Length may describe encoded transfer bytes while fetch exposes decoded body bytes.
Write chunks through the Web Streams counterwrite-web-stream
import ByteCounterStream from 'byte-counter';
const counter = new ByteCounterStream();
const drain = counter.readable.pipeTo(new WritableStream({write() {}}));
const writer = counter.writable.getWriter();
await writer.write(new TextEncoder().encode('alpha'));
await writer.write(new TextEncoder().encode('beta'));
await writer.close();
await drain;
console.log(counter.count);
// 9Consume the readable side while writing. Awaiting writes with no reader can stall when stream backpressure fills the internal queue.
Count bytes while copying a Node filecopy-node-file
import fs from 'node:fs';
import {pipeline} from 'node:stream/promises';
import ByteCounterStream from 'byte-counter/node';
const counter = new ByteCounterStream();
await pipeline(
fs.createReadStream('input.bin'),
counter,
fs.createWriteStream('output.bin'),
);
console.log(`Copied ${counter.count} bytes`);Use the /node entry point. The default export implements Web Streams and cannot be inserted directly into a classic Node pipe chain.
Count a Node readable without saving itcount-node-readable
import {Writable} from 'node:stream';
import {pipeline} from 'node:stream/promises';
import ByteCounterStream from 'byte-counter/node';
const counter = new ByteCounterStream();
const sink = new Writable({
write(_chunk, _encoding, callback) {
callback();
},
});
await pipeline(source, counter, sink);
console.log(counter.count);A transform still needs a downstream consumer. pipeline propagates source, counter, and sink errors and resolves only when all stages finish.
Count a Node HTTP responsecount-http-download
import {pipeline} from 'node:stream/promises';
import fs from 'node:fs';
import ByteCounterStream from 'byte-counter/node';
const counter = new ByteCounterStream();
await pipeline(response, counter, fs.createWriteStream('download.bin'));
console.log({status: response.statusCode, bytes: counter.count});Validate the HTTP status before committing the file; the counter treats an error response body like any other bytes.
Count bytes written to an HTTP requestcount-http-upload
import {pipeline} from 'node:stream/promises';
import fs from 'node:fs';
import ByteCounterStream from 'byte-counter/node';
const counter = new ByteCounterStream();
await pipeline(fs.createReadStream('archive.tar'), counter, request);
console.log(`Request body: ${counter.count} bytes`);This counts body bytes passed to the request stream, not HTTP headers or lower-level framing added by the network stack.
Sample a running Node transfersample-node-progress
const counter = new ByteCounterStream();
const timer = setInterval(() => {
console.log(`${counter.count} bytes processed`);
}, 1000);
try {
await pipeline(source, counter, destination);
} finally {
clearInterval(timer);
}
console.log(`Final: ${counter.count}`);The library emits no progress event. Sampling count is simple, but percentage and ETA require a known total plus your own timing logic.
Add a hard size limit after the counterreject-oversized-stream
import {Transform} from 'node:stream';
const limit = new Transform({
transform(chunk, _encoding, callback) {
if (counter.count > 10 * 1024 * 1024) {
callback(new Error('Body exceeds 10 MiB'));
return;
}
callback(null, chunk);
},
});
await pipeline(source, counter, limit, destination);byte-counter only observes. A separate stage must fail the pipeline, and placing it after the counter lets it inspect the updated total for the current chunk.
Use a fresh counter per transferisolate-transfer-counts
async function copyWithSize(source, destination) {
const counter = new ByteCounterStream();
await pipeline(source, counter, destination);
return counter.count;
}
const firstBytes = await copyWithSize(firstSource, firstDestination);
const secondBytes = await copyWithSize(secondSource, secondDestination);There is no reset method. Reusing one instance would accumulate its count and stream state rather than produce independent totals.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| progress-stream | npm | Node streams that need percentage, speed, elapsed time, and ETA callbacks rather than only a byte total |
| stream-meter | npm | Node-only code that also wants to stop a stream after a configured maximum size |
| byte-length | npm | You only need a function that measures a string or Buffer and do not need a transform stream |
| bytes | npm | You need to parse and format human-readable sizes such as 5 MB rather than count streamed data |