13 views
await Body.transformToByteArray() on a 8GB object is how Node services OOM in production. The correct pattern is pipeline() from S3’s readable stream through transforms into the destination (HTTP response, another PutObject, or disk) while honoring backpressure and verifying checksums incrementally. “Zero-copy” here means zero extra full-object buffers — not magic without copies in V8.
⚡ TL;DR: Use@aws-sdk/client-s3withBodyasReadable; wrap inpipelinewithcreateHashtransforms; set highWaterMark deliberately; never concatenate chunks into oneBufferfor large objects. Cross-link: Brotli vs Gzip in Node for streaming compression and Lambda + Bedrock streaming for the same backpressure mindset.
Zero-Copy Node Streams: production guidance
typescript
// ❌ Full object in memory
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
const out = await s3.send(new GetObjectCommand({ Bucket, Key }));
const bytes = await out.Body!.transformToByteArray(); // boom on large keys
typescript
// ✅ Stream with backpressure + incremental checksum
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { pipeline } from "node:stream/promises";
import { createHash } from "node:crypto";
import { createGunzip } from "node:zlib";
import { Readable, Transform } from "node:stream";
const s3 = new S3Client({});
function hashTap(algo: string) {
const h = createHash(algo);
return new Transform({
transform(chunk, _e, cb) {
h.update(chunk);
cb(null, chunk);
},
flush(cb) {
(this as any).digestHex = h.digest("hex");
cb();
},
});
}
export async function mirrorS3(srcBucket: string, key: string, dstBucket: string) {
const got = await s3.send(new GetObjectCommand({ Bucket: srcBucket, Key: key }));
const body = got.Body as Readable;
const tap = hashTap("sha256");
const upload = new Upload({
client: s3,
params: {
Bucket: dstBucket,
Key: key,
Body: body.pipe(tap), // still prefer pipeline() below for error propagation
ContentType: got.ContentType,
},
queueSize: 16 * 1024 * 1024,
});
// Better: explicit pipeline into a PassThrough feeding Upload
await upload.done();
const digest = (tap as any).digestHex as string;
if (got.ChecksumSHA256 && got.ChecksumSHA256 !== digest) {
// Note: S3 checksum headers may be base64 — normalize before compare
throw new Error("checksum_mismatch");
}
return digest;
}
pipeline() for HTTP egress
typescript
import { pipeline } from "node:stream/promises";
import type { IncomingMessage, ServerResponse } from "node:http";
export async function downloadHandler(req: IncomingMessage, res: ServerResponse, key: string) {
const got = await s3.send(new GetObjectCommand({ Bucket: process.env.BUCKET!, Key: key }));
res.writeHead(200, {
"Content-Type": got.ContentType ?? "application/octet-stream",
"Content-Length": got.ContentLength ? String(got.ContentLength) : undefined,
ETag: got.ETag,
});
await pipeline(got.Body as Readable, res);
}
highWaterMark and concurrency
Default chunking can still blow RSS if you fan out dozens of parallel pipelines. Cap concurrent large transfers per process and tunehighWaterMark (often 64KB–1MB) based on profile data from Node Flamegraphs on ECS.
typescript
import PQueue from "p-queue";
const queue = new PQueue({ concurrency: 4 });
export function enqueueMirror(job: () => Promise<void>) {
return queue.add(job);
}
Checksums and multipart
- Prefer S3 object checksums (CRC32C/SHA256) and validate when present.
- For multipart uploads, use
@aws-sdk/lib-storageUploadso parts stream without a full local file. - On gunzip transforms, verify uncompressed size against an allowlist to avoid zip-bombs.
typescript
export async function readGunzipLimited(src: Readable, maxBytes: number) {
let n = 0;
const limiter = new Transform({
transform(chunk, _e, cb) {
n += chunk.length;
if (n > maxBytes) cb(new Error("uncompressed_limit"));
else cb(null, chunk);
},
});
// pipeline(src, createGunzip(), limiter, dest)
return { limiter };
}
Closing checklist
✅ Dos– ✅
pipeline() from GetObject Body to destination– ✅ Incremental hashes / S3 checksum verification
– ✅ Cap concurrent large transfers
– ✅ Use multipart
Upload for big PutObject paths– ✅ Bound decompression sizes ❌ Don’ts
– ❌ Don’t
transformToByteArray() / Buffer.concat multi-GB bodies– ❌ Don’t ignore pipeline errors (orphaned sockets)
– ❌ Don’t fan out unlimited parallel mirrors
– ❌ Don’t trust client-provided Content-Length alone
– ❌ Don’t disable backpressure by buffering in custom transforms
Related reading
- Brotli vs Gzip in Node: CPU Tradeoffs at the CDN Edge
- Lambda + Bedrock: Stream Tokens Without API Gateway Caps
- Node Flamegraphs on ECS: Continuous Profiling That Survives Scale
- Node Fetch vs Got vs Undici: Throughput Under Real TLS Churn
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
