Reading a timeline
Each step is an interval, steps are listed only in a proven order, and steps no fact can order are shown together as concurrent, order unknown.
A timeline is the list of everything Trailbeam knows happened to one message ID: the send, each delivery, visibility changes, deletes, Lambda outcomes, steps Trailbeam inferred, and a dead-letter move SQS reported. The web screen at /message?id=<message ID> and GET /api/messages/{message_id} show the same object.
The summary#
| Field | Values | How it is decided |
|---|---|---|
state |
deleted | An observed delete with status final exists |
| deleted_inferred | The latest receive is a Lambda receive and its outcome is success |
|
| processing | The latest receive is a Lambda receive with no outcome, still within lambda_timeout_s |
|
| not_deleted | Anything else | |
source_queue |
queue name | Queue of the first send, else of the first receive |
current_queue |
queue name | Queue of the latest receive, else source_queue |
in_dead_letter_queue |
bool | current_queue is a queue SQS reported moving the message to. A receive on another queue without that report does not count |
dead_letter_queue_move |
observed, not_seen_on_dlq_yet or null |
observed: SQS reported a move. not_seen_on_dlq_yet: the receive count on source_queue has reached its configured maxReceiveCount, the message is not deleted, and no dead-letter receive has reported a move yet. null otherwise, including when maxReceiveCount is not configured |
deliveries |
int | Observed receives, on any queue |
successful_deliveries |
int | Deliveries that ended in an accepted delete (once per delivery) or a Lambda success. Above 1 means the work ran more than once |
first_seen, first_seen_clock |
time, host or sqs |
The send's end on the producer's clock. Only without a send: the earliest SentTimestamp, on SQS's clock |
concurrent_groups |
int | Groups with more than one event |
"Latest receive" means the receive with the latest end time.
Each step is an interval#
An observed step shows two times: just before its SQS request was sent, and when the response came back. On the screen:
2026-11-14 12:00:01.204000Z to 2026-11-14 12:00:01.231000Z (27.0 ms)SQS acted somewhere in that interval; Trailbeam cannot see exactly when. Durations under 10 s are shown in milliseconds with one decimal, longer ones in seconds.
In the API each observed event has start, end, start_unix_nano, end_unix_nano (strings, since nanoseconds overflow a JavaScript number), start_observed, and time / time_unix_nano (equal to end).
A Lambda receive has start: null and start_observed: false. Lambda's poller made the ReceiveMessage call, so the wrapper only knows it happened no later than the moment the event arrived. The screen shows no later than <time>.
Step types and their screen labels#
type |
Screen label | Key fields |
|---|---|---|
sent |
Sent | batch_entry_id |
received |
Received | delivery (1, 2, ... by the receives' end times), receive_count, sent_timestamp, first_receive_timestamp |
deleted |
Delete | delivery, result, status, error_code |
visibility_changed |
Visibility changed | delivery, result, visibility_timeout_s |
lambda_outcome |
Lambda outcome | outcome, error_type, delivery |
visibility_timeout_expired |
Visibility timeout expired | inferred |
deliveries_not_observed |
Deliveries not observed | inferred |
deleted_by_lambda |
Deleted by Lambda | inferred |
lambda_invocation_ended_without_outcome |
Lambda invocation ended without an outcome | inferred |
moved_to_dead_letter_queue |
Moved to dead-letter queue | reported by SQS, no time of its own |
delivery on a delete or visibility change is the delivery whose receipt handle was used, found by the hash join. A delete's status is one of final, stale_handle, failed or unmatched_handle (see Receipt-handle hashing).
Every step also names its consumer: service_name, client_id, source (client or lambda), pid, thread_name, faas_name, faas_invocation_id, whichever the span carried. span gives trace_id and span_id to look the call up in a tracing backend.
Order is shown only when proven#
Step A is listed above step B only if an observed fact proves A came first: the clocks, the call order in one process, SQS's receive count, or SQS's timestamps. Trailbeam never orders steps by what caused what. Each step names the fact under it on the screen. In the API, each step's after list names the steps it immediately follows, with sources and a why sentence, for example:
{"event": "e2", "type": "received", "sources": ["clock", "process_order"], "source": "clock",
"why": "Receive 1 ended (2025-11-14T00:26:40.020000Z) before this started (2025-11-14T00:27:18.900000Z), on the hosts' clocks. Same process: receive 1 returned before this call was issued."}The screen prints each entry as After receive 1: <why>. The four sources and their exact rules are in How steps are ordered.
Groups and concurrent boxes#
Trailbeam lists all steps in one order that agrees with every proven edge, then cuts the list at every point where everything above the cut is proven before everything below it. Each piece is a group (group: 0, 1, 2, ...).
- A group of one step is drawn as a normal step. An inferred step has a dashed, striped border and an
inferredbadge, with its time window and reason instead of a time. - A group of more than one step is drawn as one dashed box with a
concurrentbadge and the label "concurrent, order unknown". - An inferred step that sits alone between two groups gets
group: null.
Inside a box:
- each step is a bar on one time scale shared by the box (a waterfall); inferred steps and dead-letter moves are drawn as hatched windows;
- rows are sorted by request start time. That is not the order SQS acted in;
- a step whose start is unknown (a Lambda receive) is drawn from the left edge and listed first;
- some pairs may still be ordered. Their
afterentries say so, andordered_insideon the group counts them; - the box can hold two steps that do not overlap, when both overlap a third step in the box (a chain of overlaps is one group).
Each groups[] entry has group, concurrent, label, observed_events, inferred_events, ordered_inside, start (null if any member's start is unknown) and end.
Worked example#
A long-polling consumer, a visibility timeout of 30 s, and slow processing. Receive 1's poll was already waiting when the message was sent.
- 12:00:01.204Sentorders-api · 12:00:01.204 to 12:00:01.231 (27.0 ms)
- 12:00:01.229Receivedworker-a · delivery 1 · receive count 1 · poll started 12:00:00.100
- inferredVisibility timeout expiredafter delivery 1, before delivery 2
- 12:00:31.410Receivedworker-b · delivery 2 · receive count 2
- 12:00:31.902Deleteworker-b · result ok · status final
- 12:00:33.118Deleteworker-a · result ok · status stale_handle
What Trailbeam can prove here:
- Receive 1 ended (01.229) before the send ended (01.231), and its start (00.100) is before the send's. The clocks prove nothing. If SQS's
SentTimestampis earlier thanApproximateFirstReceiveTimestamp,sqs_timestampsorders the send first. If they are the same millisecond, the send and receive 1 are one concurrent box. - Receive 1 is before receive 2 by
clockandreceive_count. - worker-b's delete is after receive 2 by
process_ordereven if both fall in the same millisecond. - worker-a's late delete is after receive 1 (
clock,process_order) and after receive 2 byclock. It used the handle of delivery 1, so its status isstale_handle, andsuccessful_deliveriesis 2.
Edge cases#
| What you see | Cause | What to do |
|---|---|---|
| A delete listed above the receive whose handle it used | The deleting host's clock is behind. Steps are never re-ordered by cause | Check delivery on the delete: it still names the right delivery. Fix host clock sync |
An order_conflict notice and a larger box |
Sources contradicted each other, for example a clock that jumped back between two calls of one process | Read order.conflicts for the dropped edges |
| A Lambda receive inside a box with the step before it | Its start is unknown, so neither clock nor process_order can put it after anything |
Expected. receive_count and sqs_timestamps can still order it |
Two receives with the same receive_count, no expiry shown |
Standard queues deliver at least once; two consumers got the same delivery | Use duplicate processing |
first_seen_clock: "sqs" |
The send was not observed | Do not compare first_seen with the other times without allowing for clock skew |
send_not_observed notice |
Producer not instrumented, or the send is older than the data | Add the hooks to the producer |
Related#
Timeline
Message ID
MessageId, assigned by SQS when the message is sent. It stays the same on every receive, including a receive from a dead-letter queue. Trailbeam joins everything about a message by this ID.Amazon SQS terms in Vocabulary
Delivery
Change visibility
ChangeMessageVisibility: sets a new visibility timeout on an in-flight message, counted from the call. A value of 0 makes it visible at once.Amazon SQS terms in Vocabulary
Inferred
inferred: true. See Recorded and inferred steps.Receive
ReceiveMessage call returning a message. A receive does not remove the message; it starts its visibility timeout.Amazon SQS terms in Vocabulary
Queue
Amazon SQS terms in Vocabulary
Message
Amazon SQS terms in Vocabulary
Receive count
ApproximateReceiveCount: how many times SQS has returned the message. It goes up by one on every receive.Amazon SQS terms in Vocabulary
maxReceiveCount
Amazon SQS terms in Vocabulary
Delete
DeleteMessage, made with a receipt handle. This is the only way a consumer removes a message. With the handle of an older receive, the call can still succeed while the message is received again.Amazon SQS terms in Vocabulary
Producer
SendMessage or SendMessageBatch).Amazon SQS terms in Vocabulary
SentTimestamp
Amazon SQS terms in Vocabulary
Recorded (observed)
inferred: false.Step
Visibility timeout
Amazon SQS terms in Vocabulary
Dead-letter queue (DLQ)
Amazon SQS terms in Vocabulary
Receipt handle
Amazon SQS terms in Vocabulary
Span
OpenTelemetry terms in Vocabulary
Long polling
ReceiveMessage with WaitTimeSeconds above 0, which waits up to that long for a message instead of returning empty at once.Amazon SQS terms in Vocabulary
Consumer
ReceiveMessage) and then deletes them. A Lambda function behind an event source mapping is a consumer too.Amazon SQS terms in Vocabulary
ApproximateFirstReceiveTimestamp
Amazon SQS terms in Vocabulary
Hooks
trailbeam.boto.install()), the AWS SDK v3 middleware (trailbeam.instrument(client)) and the Lambda wrapper.