Tell vs Ask
- Tell: Use case: Fire-and-forget. Response: None.
- Ask: Use case: Request-response. Response: Future you can await.
Use Tell when you don’t need a reply: logging, notifications, commands that trigger side effects. Use Ask when
you need a result: queries, computations, or any flow that depends on the response.
The Ask timeout reaches the actor
The timeout of an Ask is not only how long the caller waits. The actor knows the moment the caller stops waiting: the
ask timeout, or the deadline of the caller’s context when that comes first.
- An Ask that expired before its turn is skipped. When the actor dequeues an Ask whose caller already gave up, in
the mailbox or in the stash, it does not hand it to
Receive. Under load this keeps a slow actor from spending its
turns on answers nobody reads, and a caller that timed out and retried does not get its work done twice.
Context() ends with the Ask. Inside Receive, ctx.Context() carries that deadline, so a handler that passes
it to a database query or an HTTP call stops when nobody waits for the answer. The context also ends when Receive
returns.
- A timeout is
ErrRequestTimeout everywhere. errors.Is(err, errors.ErrRequestTimeout) is true whether the actor
is on the calling node or on another one.
A Tell is not affected: it is handled whenever its turn comes, and its context carries no deadline of its own.
An Ask that is already in Receive when its caller gives up still runs to the end, unless the handler watches
ctx.Context(). The caller sees ErrRequestTimeout while the work may have been done, so a handler whose callers
retry must be idempotent.
Message types
All message-passing APIs accept any. You can send:
- Plain Go structs (with CBOR serialization when remote)
- Protocol buffer messages (default ProtoSerializer)
- Any type registered with the serializer
The framework handles serialization when the target is on another node. Locally, messages are passed by reference
without serialization.
Keep messages pure
Treat every message as a pure data struct: fields should carry only values the receiver needs to process the message
(IDs, payloads, commands, query parameters). Prefer immutable messages: do not mutate a message after it has been
sent, and do not share mutable references between actors through message fields.
Do not embed concurrency primitives in messages to collect a reply: especially chan fields, mutexes, wait
groups, or callbacks. That couples the sender’s goroutine to the actor’s mailbox, breaks the actor model, and can
cause races, deadlocks, or unpredictable behavior when messages are stashed, forwarded, retried, or sent remotely
(channels and function values do not serialize).
When you need a response, use Ask and reply with Response, or send a separate reply message with Tell. See
Tell vs Ask and ReceiveContext messaging methods.
Message ordering
Messages between a specific sender-receiver pair are delivered in the order they were sent (FIFO). This follows from the
mailbox being a FIFO queue and the single-threaded processing guarantee per actor.
Sender context
In Receive, you can access the sender via ctx.Sender(). It returns a *PID or nil (e.g., for scheduled messages
or when the sender is unknown). Use ctx.Sender().Path() when you need the sender’s address for a reply.
ReceiveContext messaging methods
Inside Receive, the ReceiveContext provides these messaging operations:
- Tell: Fire-and-forget. Does not block.
- Ask: Request-response. Blocks until reply or timeout.
- Response: Reply to an Ask. Call exactly once per Ask message.
- Request: Non-blocking Ask; use continuations for the reply.
- PipeTo: Run task asynchronously; deliver result to
to. See PipeTo.
- PipeToName: Same, target by name.
Use Sender() to get the sender’s PID when replying. Use ActorSystem().ActorOf(ctx, name) to resolve an actor by name
before sending.
No shared state
Actors never share memory. All communication is through pure, preferably immutable message structs: not shared channels,
mutable bags of state, or callbacks stuffed into the payload. That discipline eliminates whole classes of concurrency
bugs and keeps behavior predictable under remoting, supervision, and mailbox scheduling.