Netstrings for Network Message Framing and Bounded Parsing
Corrected educational edition. Original English publication: 2026-07-16T11:17:57.687+03:00. The Hebrew edition uses its actual publication date, not the English original date. All code examples are runtime-untested; no benchmark or first-hand testing is claimed. English original
Hey everyone, it’s Alex here from Coding with Alex. Welcome back to the blog.
Stream framing and serialization solve different problems. JSON, XML and Protocol Buffers describe payloads; TCP still needs message boundaries. Daniel J. Bernstein documented Netstrings in a 1997 Internet-Draft: a small length-prefixed encoding that can frame arbitrary bytes, including a serialized JSON payload. This is not evidence of a universal speed advantage. [source 1]
I’m talking about Netstrings.
Netstrings remain a useful design option for small custom protocols, including telemetry and internal services. This article explains their byte format, implementation trade-offs and resource limits. It does not claim a measured resurgence, superiority over modern protocols, or a security or latency guarantee. [source 1]
The Problem: Framing in a Streaming World
Before we look at Netstrings, let’s talk about the fundamental problem they solve: framing.
TCP exposes an ordered byte stream, not application-message boundaries. Separate writes can be combined in a read or split across reads; application framing must handle both. TCP itself does use segments on the network. [source 1]
To reconstruct the original messages, you need a way to determine where one message ends and the next begins. Developers usually solve this in one of two ways:
- Delimiter framing scans for a terminator such as a newline or NUL byte. Arbitrary binary payloads require escaping or another agreed encoding; restricted payload formats may already exclude the terminator. Bounded buffering and strict parsing are needed to prevent resource exhaustion and ambiguity. [source 1]
- Length-prefix framing sends a size followed by the payload. A fixed-width binary prefix needs an agreed byte order, not dependence on either CPU architecture. An ASCII decimal prefix is easier to inspect manually but still needs bounds and strict digit validation. [source 1]
Netstrings combine a decimal length prefix with a trailing comma. The prefix is readable; the payload may be binary. The design permits early size checks but does not supply application security by itself. [source 1]
What is a Netstring?
Bernstein’s archived Internet-Draft specifies the following encoding for a byte string. It is a historical proposal, not an IETF RFC or Internet standard. [source 1]
[len]:[string],Here is how it breaks down:
- [len]: The length of the payload, represented as an ASCII string of decimal digits (no leading zeros allowed, except for the number "0" itself).
- :: A literal colon character serving as a separator.
- [string]: The raw payload byte string, which can contain absolutely any binary data (including null bytes, colons, commas, or newlines).
- ,: A literal comma character serving as the terminator.
The ASCII string hello world! occupies 12 bytes and is encoded as follows. Count encoded bytes, not Unicode characters: a Hebrew string in UTF-8 generally uses more than one byte per character. [source 1] [source 2]
12:hello world!,What if we want to send an empty string? Simple:
0:,Because the payload length is declared up front, the receiver knows exactly how many bytes to read after the colon. It doesn't have to scan the payload for delimiters, handle escape characters, or worry about binary byte-ordering.
Practical properties of Netstrings [source 1]
Netstrings can frame JSON or Protobuf bytes; they do not replace those formats’ data models, schema tools or ecosystem. Their advantages concern simple framing and validation. [source 1]
1 Resource limits rather than DoS immunity [source 1]
An unbounded delimiter parser can exhaust memory while waiting for a terminator. This is distinct from a memory-corruption buffer overflow. Either delimiter or length-prefix parsers can be made unsafe if they accept unbounded input or mishandle arithmetic. [source 1]
A receiver can reject a declared 50 MB payload when its policy permits at most 1 MB, before allocating that payload. It must also limit the prefix digits, validate every byte, reject noncanonical leading zeros, impose connection and frame deadlines, and bound concurrent clients. A short valid header does not stop a slow sender or connection flood. [source 1] [source 2]
2 Buffer views and copying trade-offs [source 1]
If a complete frame is already contiguous, Buffer.subarray can expose its payload without copying. Reassembling fragmented input may still copy data; Buffer.concat copies bytes into a new buffer, and a small view can retain a large backing allocation. The corrected example below deliberately allocates at most one bounded payload and copies incoming bytes into it. It is not zero-copy. [source 1]
3. Transport and Architecture Agnostic
Because the length prefix is sent as ASCII text (e.g., 1024), you don't have to worry about whether the sender is a 32-bit big-endian mainframe or a 64-bit little-endian ARM chip. The encoding is universal, readable, and incredibly easy to debug using standard network tools like netcat, tcpdump, or wireshark.
Implementing a Netstring Parser in Node.js / TypeScript
The following runtime-untested TypeScript example uses a length/payload/comma state machine, validates ASCII digits without parseInt, rejects leading zeros, caps each payload at 1 MiB and detects truncated input. Each connection retains at most one bounded payload, excluding Node’s socket buffers and data retained by the application. Review and test it before use; it is not a production security guarantee. [source 1] [source 2] [source 3]
// Runtime-untested illustrative TypeScript; review before deployment.
import { Buffer } from 'node:buffer';
export class NetstringParser {
private length = 0;
private digits = 0;
private leadingZero = false;
private state: 'length' | 'payload' | 'comma' = 'length';
private payload = Buffer.alloc(0);
private offset = 0;
public frameStartedAt: number | null = null;
constructor(private onMessage: (payload: Buffer) => void,
private maxPayload = 1024 * 1024) {
if (!Number.isSafeInteger(maxPayload) || maxPayload < 0) {
throw new Error('Invalid payload limit');
}
}
write(chunk: Buffer): void {
let i = 0;
while (i < chunk.length) {
if (this.state === 'length') {
const byte = chunk[i++];
if (this.digits === 0) this.frameStartedAt = performance.now();
if (byte === 0x3a) {
if (this.digits === 0) throw new Error('Empty length');
this.payload = Buffer.alloc(this.length);
this.offset = 0;
this.state = this.length === 0 ? 'comma' : 'payload';
} else {
if (byte < 0x30 || byte > 0x39 || this.leadingZero) {
throw new Error('Invalid length');
}
if (++this.digits > String(this.maxPayload).length) {
throw new Error('Length prefix too long');
}
this.leadingZero = this.digits === 1 && byte === 0x30;
this.length = this.length * 10 + byte - 0x30;
if (!Number.isSafeInteger(this.length) || this.length > this.maxPayload) {
throw new Error('Payload too large');
}
}
} else if (this.state === 'payload') {
const count = Math.min(chunk.length - i, this.length - this.offset);
chunk.copy(this.payload, this.offset, i, i + count);
this.offset += count;
i += count;
if (this.offset === this.length) this.state = 'comma';
} else {
if (chunk[i++] !== 0x2c) throw new Error('Missing comma');
const complete = this.payload;
this.state = 'length';
this.length = this.digits = this.offset = 0;
this.leadingZero = false;
this.payload = Buffer.alloc(0);
this.frameStartedAt = null;
this.onMessage(complete); // Callback must not reenter write().
}
}
}
end(): void {
if (this.state !== 'length' || this.digits !== 0) {
throw new Error('Truncated netstring');
}
}
}
This runtime-untested local acknowledgement-server example adds idle and frame timeouts, closes malformed input, and pauses future socket reads when output needs backpressure; parsing of the current chunk continues. It binds only to loopback. Authentication, encryption, global connection/rate limits, graceful shutdown and adversarial tests remain separate deployment work. [source 1]
// Runtime-untested; NetstringParser is defined above.
import * as net from 'node:net';
const server = net.createServer((socket) => {
const parser = new NetstringParser((_payload) => {
if (!socket.write('2:OK,')) socket.pause();
});
socket.on('drain', () => socket.resume());
socket.setTimeout(5000, () => socket.destroy());
const timer = setInterval(() => {
const started = parser.frameStartedAt;
if (started !== null && performance.now() - started > 5000) socket.destroy();
}, 250);
timer.unref();
socket.on('data', (chunk) => {
try { parser.write(chunk); } catch { socket.destroy(); }
});
socket.on('end', () => {
try { parser.end(); } catch { socket.destroy(); }
});
socket.on('error', () => socket.destroy());
socket.on('close', () => clearInterval(timer));
});
server.on('error', (error) => console.error(error.message));
server.listen(8080, '127.0.0.1');
Where are Netstrings Used Today?
Distinguish documented uses of Netstrings from other length-prefixed encodings and potential applications: [source 1]
- SCGI: a web-server/application protocol whose request headers are carried in a netstring, followed by a separate request body. It is not accurate to describe every part of SCGI as a netstring. [source 1]
- Bernstein’s specification is a direct source for the format. The original claim that Qmail and djbdns use Netstrings exclusively for IPC is not established here and must not be used as an adoption or security claim. [source 1]
- RESP: Redis bulk strings also use an explicit byte length, but RESP has its own type markers, CRLF rules and aggregate types. It is a separate protocol, not a compatible Netstrings variant. [source 1]
- IoT and embedded designs: Netstrings are a possible choice where a small parser and bounded messages fit the requirements. Suitability depends on payload sizes, RAM, transport and device constraints; no broad adoption or in-place parsing claim is made here. [source 1]
Wrapping Up: When to Use Netstrings
Netstrings are not a universal replacement for established protocols. Browser APIs often use JSON over HTTP, while schema-based RPC or data formats may fit complex services. Choose separately the transport, framing, serialization, authentication and observability you need. [source 1]
However, if you are building:
- Lightweight internal microservices
- High-performance TCP-based daemon processes
- Inter-process communication (IPC) over Unix domain sockets
- IoT/embedded data pipelines with restricted compute budgets
For these cases, Netstrings are worth evaluating when a simple byte-frame format is useful. Encoding is straightforward, but secure production parsing requires bounds, deadlines, resource control and tests. Code length is not a security criterion. [source 1]
What do you think? Have you used Netstrings or a similar length-prefixed protocol in your systems? Or do you prefer sticking to JSON/msgpack streams? Let me know in the comments below!
Until next time, happy coding!