Queues · Lesson 3
SQS is pull-based (Lesson 1), so unlike a push broker, you write the loop that asks for work. How you ask — how long you wait each time — decides your bill and your latency. This is the loop inrunner.goanddispatcher.go, demystified.
A consumer calls ReceiveMessage in a loop. The one knob that matters
is WaitTimeSeconds — how long SQS may hold your request open
before answering. Two settings, very different behaviour:
| Short poll (wait = 0) | Long poll (wait = 1–20s) | |
|---|---|---|
| Answers | immediately | as soon as a message lands, else at timeout |
| Servers checked | a sample of SQS's servers | all of them |
| Empty replies | frequent — even when messages exist | rare |
| Requests/min on an idle queue | thousands | ~3 |
dispatch.Config{} (all zero values), and
withDefaults() turns a zero ReceiveWaitTime into
20s; only a negative value opts into short polling. So
both loops long-poll. Trust the code over anyone's summary — including mine.
The control queue (runner.go) is the simple
shape: one long-poll receive per cycle, up to 10 messages, fan each out to a
goroutine, ack on success.
// runner.go
const longPoll = 20 * time.Second // "20s minimises empty receives"
for ctx.Err() == nil {
msgs := receive(ctx, batch=10, wait=longPoll) // one long poll
for _, m := range msgs { go handle(m) } // fan out
}
The per-run task queues (dispatcher.go) are the
interesting case: there isn't one queue, there are hundreds (one per live
run). Conveyor runs a pool of 100 goroutines; each picks a random
active queue and long-polls it for up to 10 messages.
Both loops pass MaxNumberOfMessages = 10 (the SQS cap). One request
can return up to ten messages, so you pay one round-trip's latency and cost to get
ten units of work. Free throughput — always batch unless you have a reason not to.
Pull. The polling strategy is your problem — long-poll or you burn money on empty receives.
The consumer holds an open connection and the broker streams / pushes messages as they arrive. No empty-poll tax — there's nothing to poll.
Blocking reads (XREAD BLOCK, JetStream pull with expiry) are
basically long polling under another name — same idea, same win.
Primary source: AWS
· Amazon SQS short and long polling — it explains the server-sampling reason
short polls come back empty. Then read internal/worker/runner/runner.go
top to bottom (it's ~100 lines) and the dispatchWorker /
pickRandom functions in dispatcher.go.
Curious why conveyor runs its own goroutine pool instead of a Lambda SQS event source mapping, or how the random-pick fairness holds up under load? Ask me.