At-least-once
Every record is delivered at least once, and may be delivered more than once. Duplicates happen for ordinary reasons: a delivery that timed out after your receiver had already stored the batch, a worker that restarted mid-flight, a retry that overlaps a slow success. They are not a fault condition. Ordering is not promised either. Records are claimed oldest-first and a failed batch goes back to the queue, so a retried record can arrive after a newer one. Sort bytimestamp, and within one execution by lua.execution.seq, rather than trusting arrival order.
Batching
Records are collected continuously and flushed about every 2 seconds. A batch is cut when the destination’s record or byte cap is reached, whichever comes first. One batch carries exactly oneresource. Records from three agents become three batches — never one batch with a mixed resource — so your ingest never has to reconcile a resource per record.
Bodies are always gzipped. A record larger than a destination’s per-record cap has its
body cut and suffixed …[truncated for destination] rather than being dropped — the event, its severity, and its attributes still arrive.
At most 4 requests are in flight to any one drain at a time. That is what stops a slow destination starving the others: one struggling drain can occupy four delivery slots, never the pool.
Retry and backoff
Each attempt gets 10 seconds. A failure is either retryable or terminal. Retryable: a network error, a DNS failure, a timeout, and any status in the destination’s retried list above. The batch goes back to the queue with a delay drawn uniformly from zero up to a ceiling that doubles per attempt and stops at 60 seconds — full jitter, so a destination coming back up is not hit by every drain’s backlog at the same instant.
A
Retry-After header — delta-seconds or an HTTP date — overrides that and is honoured exactly, clamped at one hour.
Terminal: every 3xx (redirects are refused, never followed) and every 4xx other than 429 — and, for OTLP, 400 and 500 explicitly. A terminal failure drops the batch, counts it under rejected, and does not retry, because the same bytes would be refused again.
OTLP is the one destination where a 500 is terminal: its specification allows a client to retry 429, 502, 503, and 504, and nothing else. The other three destinations retry a 500 like any server error.
The six-hour horizon
A record that has been waiting more than 6 hours is dropped, whatever the reason for the wait. That bounds the buffer in time: an endpoint that comes back after a long outage receives the last six hours, not a six-day flood that would knock it over again.Auto-pause
A drain with no successful delivery for 24 hours, and at least one failure in that window, pauses itself with reasonauto. It keeps buffering — it just stops trying. Fix the destination, then:
degraded, never straight in healthy.
Error classes
Every attempt is recorded with one class, which is whatlua drains deliveries prints and what a support conversation starts from.
Output
Backpressure and drops
The buffer is bounded in size as well as in time. When a drain’s queue exceeds its limit, the oldest pending records are dropped — keeping the newest is what you want during an incident, because the newest records are the ones describing it. You are told, in band. On the first successful delivery after a drop, the drain sends one synthetic record:dropped_from and dropped_to bound the gap by record id, so you can reconcile against lua logs — the platform’s own copy is unaffected by a drain drop and is still readable for the full retention window.
Alert on lua.drain.dropped: it means your pipeline lost data and is the one event that cannot be inferred from the absence of something else. The monitor packs include it.
Quota degradation
Going over the daily quota degrades the drain in steps rather than cutting it off. Dropped-for-quota records count the same way, anderror records are the last to go — the ladder never drops severity WARN or ERROR before it pauses the drain.
The 80% notification is one-shot by crossing, not by state: it fires on the batch that takes the drain from under 0.8 of its allowance to at or over it, and not again until the window rolls. A drain sitting at 0.9 all afternoon is not a drain that notifies all afternoon.
Counters reset at 00:00 UTC, and a drain the ladder paused with reason
quota resumes itself at that boundary — you do not have to run lua drains resume. Like every resume it lands in degraded rather than straight back in healthy.
The drain’s own synthetic records — lua.drain.test, lua.drain.heartbeat, lua.drain.dropped, and lua.truncated.warn — do not count against the quota, so a heartbeat every five minutes never eats an allowance you are paying for, and a drain cannot be pushed over the ladder by its own bookkeeping. They do count in health.deliveredCount24h and appear in the deliveries log, which is why that number can be larger than the records your agents produced.
lua drains status --json carries quota.usedEvents, quota.usedBytes, quota.resetsAt, and quota.degradation.
Heartbeat
Every 5 minutes, a drain in statehealthy sends one synthetic record:
- It is sent only in state
healthy. Adegradedorfailingdrain stops heartbeating, which is intended: the heartbeat going missing is exactly the signal you want when deliveries start failing. - It proves the delivery path, not the selection path. A heartbeat arriving while a selector excludes every one of your agents still means no log records will come.
lua.drain.backlog is how many records are waiting. A backlog that climbs across successive heartbeats means your destination is slower than your agents.
Health states
The transitions:
paused keeps collecting records — within the six-hour horizon and the buffer size — so a drain paused for 20 minutes during a deploy loses nothing. disabled stops collecting: nothing is queued while a drain is off, and turning it back on starts from that moment.
lua drains status exits 2 when any drain is failing. 2 is also the usage exit code, so a guard that must tell a failing drain from a mistyped command reads the state field rather than the exit status.
Next steps
Datadog monitor pack
Alerts for heartbeat absence, error spikes, and drops.
Grafana dashboard
The same signals over Loki.
Event schema
The synthetic records named on this page.
lua drains
status, deliveries, pause, and resume.
