Skip to content

api: bound total in-flight ingest body bytes now that the handler buffers the whole body #544

Description

@EricAndrechek

What

POST /v1/ingest now reads the whole request body into a pooled *bytes.Buffer before decoding (internal/api/ingest.go), where it previously streamed: the NDJSON path scanned line-by-line and the array path let json.Decoder compact after each element, so peak resident bytes per request were on the order of one record.

Peak is now O(body) per in-flight request. Two details sharpen it:

  • bytes.Buffer grows by doubling, so peak allocation can exceed the body cap itself before MaxBytesReader errors.
  • maxPooledBufferBytes (1 MiB) caps what a request may hand back to the pool. It does not cap peak.

There is no semaphore or in-flight-bytes limiter anywhere in internal/api, so the bound is concurrency × maxRequestBodyBytes (16 MiB). Note there is no operator knob for that cap — maxRequestBytes is unexported and test-only — so today the only outer limit is whatever the reverse proxy imposes.

Why it was accepted

Deliberate — it is the shape the native type layer (chtypes) lands against, which needs the body addressable rather than consumed. Documented in the CHANGELOG entry and #540's description rather than silently taken.

Why it still wants a bound

The shipped compose trial policy grants public insert on clicks/events, so a default quickstart deployment is publicly writable. An operator who does not cap body size at their proxy has no backstop, and there is no in-process knob to reach for.

Fix direction

Bound total in-flight body bytes across the ingest handler (a counting semaphore admitting on Content-Length, 503/429 when the budget is exhausted), rather than lowering the per-request cap — the per-request cap is a correctness limit on payload size, not a memory limit.

Alternative if it bites before chtypes lands: reintroduce streaming. The change is localized — newRecordReader is the one place the body shape is chosen, so it means swapping its []byte parameter for an io.Reader and adjusting the two readers behind it.

Raised in review of #540.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions