Cap event capture requests at 100 events - #162
Merged
Merged
Conversation
The capture service rejects any batch over 100 events with
`400 {"error": "batch too large", "max_size": 100}`, but EventBuffer
drained its entire backlog into a single sendBatch call. Any caller with
more than 100 buffered events lost the whole batch, with only a log line
to show for it.
The buffer's maxSize did not bound this. It gates the push-triggered
flush, and only when a flush is not already running:
if (this.events.length >= this.maxSize && !this.flushing) {
await this.flush();
}
this.events.push(event);
so while a request is in flight the size check is skipped entirely and
callers keep appending for its full duration, retry backoff included.
A caller that does not await track() sees the same thing, since pushes
interleave across the await inside push.
Split each flush into chunks of at most 100 events, retrying each chunk
independently. Per-chunk retries matter: retrying the whole drained set
would resend chunks that had already been delivered.
Also lower the default maxSize from 1000 to 100 so the common path is
one request per flush. The 1000 default predates the move to the capture
service in 1.5.0, which is when this became reachable. Before that the
buffer flushed to the REST createEventBatch endpoint and the mismatch
was latent.
Reported by a daily cron that tracked 113 companies then called close().
All 113 events went out as one request and 400'd, losing a full day of
usage data.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 19, 2026
cbrady
approved these changes
Aug 19, 2026
schematic-bot
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The capture service rejects any batch over 100 events with
400 {"error": "batch too large", "max_size": 100}(capture.go:73), butEventBuffer.flush()drained its entire backlog into a singlesendBatchcall. Any caller with more than 100 buffered events lost the whole batch, with only a log line to show for it.Found by a daily cron in
schematic-dashaithat tracked 113 companies and then calledclose(). All 113 events went out as one request and 400'd, losing a full day of usage data.Why
maxSizedid not already prevent thismaxSizegates the push-triggered flush, and only when a flush is not already running:So it is a soft ceiling. The buffer grows past it whenever:
flushingshort-circuits the size check, and callers keep appending for the full duration of the request, retry backoff included.track(). Pushes interleave across theawait this.flush()yield point insidepush.The new test
never sends more than 100 events when pushes are not awaitedreproduces case 1 and produces a 200-event request against the old code.Changes
sendChunk.maxSizefrom 1000 to 100 so the common path is one request per flush and the chunking is a safety net rather than the primary path.Failure behavior is unchanged: a chunk that exhausts
maxRetriesis logged and dropped, and does not prevent later chunks from being attempted.When this became reachable
The 1000 default predates the move to the capture service. Through 1.4.x the buffer flushed via the REST
createEventBatchendpoint; 1.5.0 switched it toEventCaptureClient.sendBatchagainstc.schematichq.com/batch, which enforces the cap. The mismatch was latent before that. Anything on a caret range picked this up as a routine dependency bump, which is exactly how it reached the demo.Cross-SDK status
I checked every SDK with an event buffer.
schematic-go,schematic-java, andschematic-csharpalready hard-cap the drain at 100 and are correct.schematic-pythonandschematic-rubyhave the same unbounded-drain shape as this and get their own PRs.schematic-jssends events one at a time and is unaffected.Testing
Four new tests in
tests/unit/events.test.ts, all four confirmed failing without the source change:Full unit suite is unchanged against baseline: 83 failed / 820 passed before, 83 failed / 824 passed after. The pre-existing failures are the WASM and webpack suites, which need a build step, and are identical on a clean checkout.
Follow-ups not in this PR
stop()awaitsflush(), butflush()returns immediately whenflushingis already set, so aclose()landing during an in-flight flush silently drops everything buffered after it.maxRetrieswith backoff on a request that can never succeed.maxSizeis not exposed onSchematicOptions(onlyeventBufferIntervalis), so there was no config-level workaround available to the affected caller.🤖 Generated with Claude Code