Skip to content

Cap event capture requests at 100 events - #98

Merged
bpapillon merged 1 commit into
mainfrom
fix/cap-event-batch-size
Aug 20, 2026
Merged

Cap event capture requests at 100 events#98
bpapillon merged 1 commit into
mainfrom
fix/cap-event-batch-size

Conversation

@ryanechternacht

Copy link
Copy Markdown
Member

Problem

The capture service rejects any batch over 100 events with 400 {"error": "batch too large", "max_size": 100} (capture.go:73), but _flush drained the whole backlog into a single send_batch call.

Severity, honestly

This is hardening rather than a live outage fix. Unlike schematic-node (where the default buffer size was 1000, see schematic-node#162), max_events here already defaults to 100, so the ordinary single-producer path stays under the cap.

It is not a hard guarantee though. push appends unconditionally after its own flush:

if should_flush:
    self._flush()
    with self.lock:
        self.events.append(event)

That tail has no length check, so producers piling up behind an in-flight send can drive the backlog past max_events. Exceeding 100 in a single request needs more than 100 concurrent pushers, which is narrow but reachable for a threaded web app. A caller who raises max_events themselves is exposed directly.

I tried to write a threaded regression test for the concurrent case and could not make it fail deterministically, so I did not ship a timing-dependent test that would flake in CI. The two tests here cover the deterministic property: a backlog larger than the cap must go out as multiple requests.

Changes

Chunk each flush into requests of at most 100 events, in both EventBuffer and AsyncEventBuffer, so the cap holds regardless of max_events or producer concurrency. Each chunk retries on its own, since retrying the whole drained set would resend chunks that had already been delivered.

This matches schematic-go, schematic-java, and schematic-csharp, which all bound the drain rather than relying on the buffer size to stay small.

Testing

Two new tests in tests/custom/test_event_buffer.py, both confirmed failing without the source change (a 250-event backlog goes out as one request instead of [100, 100, 50]):

  • TestEventBufferBatchSizeCap::test_flush_splits_backlog_into_capped_requests
  • TestAsyncEventBufferBatchSizeCap::test_flush_splits_backlog_into_capped_requests

Full tests/custom/ suite passes: 161 passed.

Context

Found while fixing a daily cron that tracked 113 companies through schematic-node and lost the whole batch to a 400. I checked every SDK with an event buffer: schematic-go, schematic-java, and schematic-csharp already hard-cap the drain and need no change; schematic-node and schematic-ruby have their own PRs; schematic-js sends events one at a time and is unaffected.

🤖 Generated with Claude Code

The capture service rejects any batch over 100 events with
`400 {"error": "batch too large", "max_size": 100}`, but _flush drained
the whole backlog into a single send_batch call.

max_events already defaults to 100, so the single-producer path stays
under the cap. It is not a hard guarantee though: push() appends
unconditionally after its own flush,

    if should_flush:
        self._flush()
        with self.lock:
            self.events.append(event)

so producers piling up behind an in-flight send can drive the backlog
past max_events, and a caller may also raise max_events themselves.

Chunk each flush into requests of at most 100 events in both the sync
and async buffers, so the cap holds regardless of max_events or producer
concurrency. This matches schematic-go, schematic-java, and
schematic-csharp, which all bound the drain rather than the buffer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bpapillon
bpapillon merged commit 49e6349 into main Aug 20, 2026
4 checks passed
@bpapillon
bpapillon deleted the fix/cap-event-batch-size branch August 20, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants