Example: io.LimitReader
io.LimitReader wraps an io.Reader with a maximum read boundary. It allows downstream code to read at most n bytes from the underlying Reader while preserving the original streaming model.
It solves a common production problem: the application needs to limit how much input a downstream operation is allowed to consume without buffering the entire input in memory.
Typical use cases include:
- Limiting the amount of data read from HTTP requests or responses
- Restricting reads to a known protocol or frame boundary
- Adding a size limit before calling
io.ReadAll - Reading only the first N bytes of a large file or stream
The most important boundary to understand is:
EOF from the limited Reader does not necessarily mean EOF from the underlying Reader.
When the configured limit is reached, io.LimitReader stops reading and returns io.EOF, even if the underlying Reader still contains more data.
This distinction matters whenever the application must detect oversized input rather than silently accept the first n bytes.
Quick Start
The simplest scenario: read at most five bytes from a data source.
Output:
The limited Reader has reached EOF after five bytes, but the underlying strings.Reader still contains unread data.
This is the key behavior to remember:
The limited Reader can be exhausted while the underlying Reader is not.
The Core Contract
io.LimitReader(r, n) returns an io.Reader that reads from r but returns at most n bytes in total.
Conceptually:
The limited Reader:
- reads from the underlying Reader on demand;
- never returns more than
nbytes in total; - does not buffer the entire input;
- does not verify that the underlying input ends at
nbytes; - does not close the underlying Reader.
Once the limit is exhausted, subsequent reads return io.EOF.
The critical point is that this io.EOF may be generated by the limit boundary, not by the underlying data source.
The Critical Boundary: EOF Does Not Prove Underlying EOF
Consider:
The limited Reader returns io.EOF after five bytes.
But the underlying Reader may still contain:
Therefore:
This is the defining semantic boundary of io.LimitReader.
Three Possible Outcomes
The caller cannot distinguish the last two cases by observing the limited Reader alone.
This is why:
can safely limit how much data is read, but cannot prove that the original input was no larger than max bytes.
When to Use
Limit Input Size Without Changing the Streaming Model
When reading data from a network connection, user upload, file, or third-party service, the input size may not be fully trusted.
io.LimitReader is useful when the application needs to impose a hard upper bound on how much data a downstream operation can consume.
For example:
This combination is useful when the goal is simply to prevent io.ReadAll from consuming an unbounded amount of input.
The memory used by io.ReadAll will still depend on the amount of data it accumulates and its internal buffer capacity, but the input exposed through the limited Reader is bounded by maxSize.
The important distinction is:
io.LimitReaderlimits how much data is read. It does not determine whether the original input exceeded the limit.
Use it when the application needs a read boundary.
Do not use it alone when the application needs oversize detection.
When Not to Use It Alone
When the Application Must Reject Oversized Input
Suppose an API accepts request bodies up to 1 MB.
If the application uses:
and the client sends 2 MB, the application reads the first 1 MB and the limited Reader eventually reports io.EOF.
The read succeeds.
But the original input was too large.
The application cannot determine this from the limited Reader alone.
It cannot distinguish:
from:
by observing only the limited Reader.
For cases where exceeding the limit must be detected and rejected, read one byte beyond the allowed limit.
This turns a silent read boundary into an explicit validation boundary.
Detecting Oversized Input with max + 1
If the application must reject input larger than max, it needs to distinguish between:
and:
A simple approach is to allow the limited Reader to expose one additional byte:
The extra byte is not part of the accepted input. It is only evidence that the input exceeded the configured limit.
The important distinction is:
maxlimits how much data is accepted.max + 1provides enough information to detect whether the boundary was exceeded.
This works because io.ReadAll treats EOF as a normal termination condition. If the input contains exactly max + 1 bytes, io.ReadAll may still return err == nil, but the resulting length is max + 1, which correctly identifies the input as oversized.
For production helpers, the limit itself should also be validated. In particular, blindly evaluating max + 1 can overflow if max is already the maximum value representable by its integer type.
A defensive implementation can handle that boundary explicitly:
In most application code, max is far smaller than math.MaxInt64, so the overflow case is highly unusual. The important production principle is simply:
Do not let the
max + 1validation technique introduce an integer-overflow boundary of its own.
Production Scenario: Limiting an HTTP Response Body
When calling a third-party HTTP API, the response body should not be treated as inherently trustworthy.
A malformed server, unexpected response, or configuration error could return a much larger response than expected.
If the goal is only to protect memory, a simple limit is enough:
This protects io.ReadAll from reading an arbitrarily large response.
However, it does not reject a response larger than maxBodySize. It silently reads only the first maxBodySize bytes.
If the application must reject oversized responses, use the max + 1 pattern instead:
Key Engineering Points
io.LimitReaderlimits how much downstream code can read from the underlying Reader.- It does not accumulate data itself; memory usage comes from the consumer, such as
io.ReadAll. io.LimitReaderreturns anio.Reader, not anio.ReadCloser.- The underlying
resp.Bodyremains the caller's responsibility to close. - A successful
io.ReadAlldoes not prove that the original response body was within the configured limit. - If the application must reject oversized input, read
max + 1bytes and explicitly check the result.
Production Scenario: Reading a Fixed Protocol Boundary
io.LimitReader can also be useful when a protocol defines a maximum length for a field or frame.
For example, suppose a protocol defines the next field as containing at most 1024 bytes:
The downstream parser can consume data through field without reading beyond the configured boundary.
This is different from io.ReadFull:
If the protocol requires exactly 1024 bytes, use io.ReadFull.
If the protocol allows up to 1024 bytes and another mechanism determines the actual end of the field, io.LimitReader may be appropriate.
The distinction is important:
io.ReadFullenforces an exact-length requirement.io.LimitReaderenforces a maximum-length boundary.
Common Failure Mode
Failure: Treating Limit-Induced EOF as Real EOF
Risky Code
Why It Fails
The code looks correct because io.ReadAll returns err == nil.
However, the successful read only proves that the limited Reader was consumed successfully.
It does not prove that the original input ended within 24 bytes.
If the original input is larger, the extra data remains unread from the underlying Reader.
This can lead to:
- silently truncated input;
- incomplete JSON or protocol data being accepted;
- incorrect signature or checksum validation;
- corrupted or incomplete uploads;
- incorrect business decisions based on partial data;
- unexpected behavior when the underlying stream is reused.
The problem is especially easy to miss because ordinary tests usually cover valid, small inputs. The failure appears only when the input exceeds the configured boundary.
Correct Approach
If the application must reject oversized input, use max + 1:
For a fully defensive reusable helper, the math.MaxInt64 boundary should also be handled as shown in the previous section.
Here, the extra byte is enough to prove that the input exceeds the 24-byte limit.
The application does not need to consume the entire oversized input.
It only needs enough information to establish:
or:
This is the fundamental difference between limiting input and validating input size.
Blocking, Cancellation, and Resource Lifetime
io.LimitReader only controls the number of bytes that may be read. It does not control when a read completes.
It does not:
- support
context.Context; - set read deadlines;
- cancel a blocked read;
- close the underlying Reader.
If the underlying Reader blocks, the limited Reader can also block.
Timeouts, cancellation, and resource lifetime must be handled by the underlying object or by the surrounding system.
For example:
- HTTP clients can use request contexts and client timeouts.
- Network connections can use read deadlines.
- Resources implementing
io.Closermust be closed by the caller.
This distinction is important:
io.LimitReaderlimits bytes, not time or resource lifetime.
io.LimitReader vs. http.MaxBytesReader
These two APIs both impose read limits, but they solve different problems.
io.LimitReader is a generic Reader wrapper. It simply limits how many bytes can be read through the returned Reader.
http.MaxBytesReader is an HTTP server-specific request-body enforcement mechanism. It limits reads from an HTTP request body and returns an error when the configured limit is exceeded. It is designed to work with an HTTP server's ResponseWriter and request lifecycle.
The distinction is:
Therefore, they should not be treated as interchangeable.
Use io.LimitReader when you need a generic maximum read boundary.
Use http.MaxBytesReader when enforcing request-body limits in an HTTP server.
API Boundary Comparison
The following APIs solve different boundary problems:
The key distinction is:
Key Takeaways
io.LimitReader is a small API with an important production boundary.
Its core contract is:
Read no more than N bytes from the underlying Reader.
It does not guarantee:
The underlying input contains no more than N bytes.
Therefore:
- Use
io.LimitReaderwhen you need a maximum read boundary. - Use
io.ReadFullwhen you need an exact number of bytes. - Use
io.ReadAllwhen you intentionally want to consume the input until EOF. - Use
io.LimitReader(r, max+1)when you need to detect whether the input exceeds a maximum size. - Do not interpret
io.EOFfrom a limited Reader as proof that the underlying Reader has reached EOF. - Remember that
io.LimitReaderlimits bytes, not memory, time, cancellation, or resource lifetime. - Use
http.MaxBytesReaderwhen enforcing HTTP server request-body limits rather than treatingio.LimitReaderas an HTTP-specific enforcement mechanism.
The most important production rule is simple:
A read limit is not the same thing as input-size validation.
That distinction is where io.LimitReader is most useful—and where it is most often misunderstood.