Lambda Invoke Modes: BUFFERED Versus RESPONSE_STREAM by Client Type

Lambda Invoke Modes: BUFFERED Versus RESPONSE_STREAM by Client Type

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

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply