Transport internals
The transport is the part of the SDK that puts events on the wire. Its contract is identical across JavaScript, Python, and Ruby: send each event immediately, queue only on failure, retry with backoff, and never block or crash your app. This page documents exactly how it behaves so you can reason about delivery under load and failure.
Event lifecycle
From capture to delivery, an event moves through these steps:
- Build — the stack is parsed, SDK metadata attached, the wire-format object assembled.
- Scrub — sensitive keys and (optionally) URL query strings are redacted. See Privacy & data.
- beforeSend — your hook can mutate or drop the event.
- Send — an HTTP
POSTwith the DSN in theX-Tinymon-Keyheader is issued right away. - Settle — on success the event is discarded; on failure it enters the retry queue.
There is no timer between steps 4 and 5. Capturing one event triggers exactly one send attempt immediately.
Status code handling
| Response | Action | Rationale |
|---|---|---|
| 2xx | Delivered, discarded | Success. |
| 4xx | Dropped, no retry | A bad DSN or invalid payload won't succeed on retry — retrying would loop forever and waste the queue. |
| 5xx | Queued for retry | Server-side/transient — worth resending. |
| Network error / timeout | Queued for retry | Connection refused, DNS failure, offline — transient. |
Retry queue & backoff
Failed events go into a bounded FIFO queue. A drain runs on a backoff schedule starting at 1 second and doubling after each failed drain up to a 30 second ceiling. The first success resets the backoff to 1 second. The queue is capped at 30 events; when full, the oldest event is dropped so a prolonged outage can never grow memory without bound.
Browser unload
In the browser, a normal fetch can be cancelled when the tab is closing, which would drop any event still in flight or queued. On the pagehide event the transport switches to navigator.sendBeacon, which the browser guarantees to deliver even as the page unloads. Because beacons can't set custom headers, the DSN rides along as a ?key= query parameter instead of the X-Tinymon-Key header.
Workers per language
The contract is the same everywhere; the concurrency primitive differs to match each runtime:
- JavaScript — the event loop. Sends are in-flight promises;
flush()awaits them. No threads. - Python — a single daemon worker thread drains a send queue; an
atexithook flushes on shutdown. Standard library only (urllib,threading). - Ruby — a single background
Threadwith aMutex+ConditionVariable; anat_exithook flushes on shutdown. Standard library only (net/http).
In every case the worker is a daemon/background thread, so it never keeps your process alive on its own and never blocks your request path.
Limits & tuning
| Parameter | Value | Meaning |
|---|---|---|
| Max retry queue | 30 events | Oldest dropped on overflow. |
| Backoff start | 1 s | Delay before the first retry drain. |
| Backoff max | 30 s | Ceiling for the doubling schedule. |
| Request timeout | 5 s | Per send attempt (Python/Ruby). |
| flush() timeout | 2 s default | Caller-overridable; see Delivery & flush(). |
These are deliberately not configurable knobs — they're tuned so the SDK stays invisible. If your use case needs different behavior, drop the event in beforeSend or front the SDK with your own queue.