Stream Large Payloads Instead of Buffering in Node.js

Flags Node.js code that reads whole files or HTTP bodies into memory, raises body size limits, or pipes streams without pipeline() error handling.

What it flags

Flaggedsrc/routes/exports.ts
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import type { FastifyInstance } from 'fastify';
import { findExport } from '../db/exports';

const EXPORT_DIR = '/var/app/exports';

export async function exportRoutes(app: FastifyInstance) {
  app.get<{ Params: { id: string } }>('/exports/:id', async (req, reply) => {
    const record = await findExport(req.params.id, req.user.id);
    if (!record) return reply.code(404).send();

    // Holds the entire export in memory for every concurrent download
    const csv = await readFile(path.join(EXPORT_DIR, record.fileName));
    return reply.type('text/csv').send(csv);
  });
}
Passessrc/routes/exports.ts
import { createReadStream } from 'node:fs';
import path from 'node:path';
import type { FastifyInstance } from 'fastify';
import { findExport } from '../db/exports';

const EXPORT_DIR = '/var/app/exports';

export async function exportRoutes(app: FastifyInstance) {
  app.get<{ Params: { id: string } }>('/exports/:id', async (req, reply) => {
    const record = await findExport(req.params.id, req.user.id);
    if (!record) return reply.code(404).send();

    const stream = createReadStream(path.join(EXPORT_DIR, record.fileName));
    return reply.type('text/csv').send(stream);
  });
}

Why it matters

Buffering a whole file or body makes memory use scale with payload size multiplied by concurrent requests, so a handful of large downloads or uploads can exceed the container memory limit, and the process is killed along with every request it was serving. An oversized or missing body limit lets a single client exhaust memory with one request. In review, the buffered version looks as correct as the streaming one, and tests with small fixtures never show the difference.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Stream Large Payloads Instead of Buffering in Node.js

Applies to `**/*.js`, `**/*.mjs`, `**/*.cjs`, `**/*.ts`, `**/*.mts`, `**/*.cts`.

Review Node.js code that handles files, uploads, downloads, and HTTP bodies for payloads loaded fully into memory.

Flag changes that:
- Read files of unbounded size, such as user uploads, exports, logs, or backups, with `fs.readFile` or `readFileSync` to send, upload, or transform them instead of using `fs.createReadStream`.
- Buffer upstream responses with `arrayBuffer()`, `text()`, `blob()`, or `Buffer.concat` over every chunk when proxying or saving files of unbounded size.
- Collect a request body with `req.on('data')` into a string or array without enforcing a maximum byte count.
- Raise body parser limits far above the defaults, such as `express.json({ limit: '500mb' })` or a large Fastify `bodyLimit`, to accept file-sized payloads.
- Accept multipart uploads with in-memory storage, such as multer `memoryStorage()`, without `limits.fileSize`.
- Connect streams with `.pipe()` and no error handling, where `pipeline()` from `node:stream/promises` would propagate errors and destroy every stream in the chain.
- Build a full export, CSV, or report as one string or array from an unbounded query instead of streaming rows.

Do not flag:
- Reading small, bounded files such as config, templates, or fixtures.
- Parsing JSON API responses with a known, small size.
- Body limits left at framework defaults or lowered.

Checks files matching **/*.js, **/*.mjs, **/*.cjs, **/*.ts, **/*.mts, **/*.cts.

More Node.js agents

See all Node.js code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code