Skip to content

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.

Based on 9 repo files at 54ea61b

docs/store-api.md, docs/architecture.md, docs/glossary.md, docs/span-contract.md, server/README.md, server/src/trailbeam_store/timeline.py, server/src/trailbeam_store/order.py, server/src/trailbeam_store/web.py, server/tests/test_timeline.py

On this page

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:

text
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:

json
{"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 inferred badge, 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 concurrent badge 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 after entries say so, and ordered_inside on 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.

Timeline sample
  1. 12:00:01.204Sentorders-api · 12:00:01.204 to 12:00:01.231 (27.0 ms)
  2. 12:00:01.229Receivedworker-a · delivery 1 · receive count 1 · poll started 12:00:00.100
  3. inferredVisibility timeout expiredafter delivery 1, before delivery 2
  4. 12:00:31.410Receivedworker-b · delivery 2 · receive count 2
  5. 12:00:31.902Deleteworker-b · result ok · status final
  6. 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 SentTimestamp is earlier than ApproximateFirstReceiveTimestamp, sqs_timestamps orders 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 clock and receive_count.
  • worker-b's delete is after receive 2 by process_order even if both fall in the same millisecond.
  • worker-a's late delete is after receive 1 (clock, process_order) and after receive 2 by clock. It used the handle of delivery 1, so its status is stale_handle, and successful_deliveries is 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