Skip to main content

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.