BUFFERED is still the right default for most APIs. RESPONSE_STREAM wins when time-to-first-byte matters (SSR, token UIs, large downloads) and the client can consume an HTTP stream. Pick per entrypoint — not globally — and measure memory vs TTFT under realistic clients.
⚡ TL;DR: Mobile/JSON CRUD → BUFFERED; SSR/HTML and LLM token UIs → RESPONSE_STREAM; batch/SQS → BUFFERED (or async). Cap streamed payloads; handle client abort. Pair with Lambda Plus Bedrock streaming and Lambda Warm Pools.
Decision matrix
| Client | Mode | Why |
|---|---|---|
| Mobile app JSON < 1 MB | BUFFERED | Simple retries, full-response caches |
| Next.js SSR via Function URL | RESPONSE_STREAM | TTFT / TTFB UX |
| Bedrock token proxy | RESPONSE_STREAM | Tokens as produced |
| SQS / EventBridge worker | BUFFERED (async invoke) | No HTTP client |
| Admin CSV export 50 MB | RESPONSE_STREAM + S3 prefer | Avoid API GW 10 MB traps |
Function URL streaming handler
// stream-handler.ts — Node 20 response streaming
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
export const handler = awslambda.streamifyResponse(
async (event, responseStream, context) => {
const http = awslambda.HttpResponseStream.from(responseStream, {
statusCode: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
try {
const body = Readable.from(renderChunks(event));
await pipeline(body, http);
} catch (err) {
// ✅ Client abort surfaces here — don't retry blindly
console.error("stream_failed", err);
http.end();
}
},
);
async function* renderChunks(event: unknown) {
yield "<!doctype html><html><body>";
for await (const part of fetchParts(event)) yield part;
yield "</body></html>";
}
BUFFERED remains correct for CRUD
// ✅ Classic JSON API — buffered invoke via API Gateway
export async function handler(event: APIGatewayProxyEvent) {
const order = await getOrder(event.pathParameters!.id!);
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify(order),
};
}
Streaming a 2 KB JSON body adds complexity (partial failures, harder middlewares) with no TTFT win. See Lambda Timeouts, Retries, and DLQs for buffered retry semantics.
Load-test both modes
# illustrative — compare TTFT and RSS
k6 run --env MODE=buffered scripts/invoke-mixed.js
k6 run --env MODE=stream scripts/invoke-mixed.js
# watch: p50/p95 TTFT, Max Memory Used, 5XX on client cancel
Abort mid-stream must not leak provisioned concurrency. Alarm on Timeout spikes when clients disconnect early.
Closing checklist
✅ Dos
– ✅ Choose mode per Function URL / API entrypoint
– ✅ Stream only when TTFT or oversized body demands it
– ✅ Handle client abort; bound stream duration
– ✅ Prefer S3 pre-signed URLs for huge artifacts
– ✅ Load-test mobile vs SSR clients separately
❌ Don’ts
– ❌ Don’t enable RESPONSE_STREAM globally “for modernity”
– ❌ Don’t stream through API Gateway without checking limits
– ❌ Don’t ignore memory profiles under slow consumers
– ❌ Don’t retry streamed side effects without idempotency
Related reading
- Lambda Plus Bedrock: Stream Tokens Without API Gateway Caps
- Lambda Warm Pools
- Lambda Timeouts, Retries, and DLQs
- LLM Coding Agents on AWS
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
