Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /go-context-timeouts.md.
Verified · 8/11/2026

Context, Timeouts, and Cancellation

Every network call on the Go SDK client takes a context.Context as its first argument, so you control per-call deadlines and cancellation with the standard library instead of a custom options struct.

Every Client method in github.com/primitivedotdev/sdks/sdk-go that performs a network request takes ctx context.Context as its first argument. There is no separate RequestOptions struct like the Node and Python SDKs use: per-call deadlines, cancellation, and request-scoped values all go through the standard library's context package.

You need this whenever a call might hang longer than you want to wait, or when an upstream cancellation (a client disconnect, a shutdown signal) should abort an in-flight Send, Reply, or Forward.

Per-call timeout#

Wrap the call in context.WithTimeout and always defer cancel().

package main

import (
	"context"
	"log"
	"time"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

func main() {
	client, err := primitive.NewClient("prim_test")
	if err != nil {
		log.Fatal(err)
	}

	// Cancel after 15 seconds.
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	_, err = client.Send(ctx, primitive.SendParams{
		From:     "Support <support@example.com>",
		To:       "alice@example.com",
		Subject:  "Hello",
		BodyText: "Hi there",
	})
	if err != nil {
		log.Printf("send failed: %v", err)
	}
}
Tip

For wait mode (Wait: true on Send or Reply), give the context a deadline long enough for the downstream SMTP transaction to settle, typically 30-60 seconds. WaitTimeoutMs (default 30000 ms, and validated to be between 1000 and 30000 ms when set) governs how long Primitive itself waits for a delivery outcome; your context deadline must be at least that long or you'll cancel the request before Primitive replies.

Per-call cancellation#

Use context.WithCancel and call cancel() from wherever the abort signal arrives, for example a user closing a connection or your own shutdown handler.

// Requires: context, primitive "github.com/primitivedotdev/sdks/sdk-go"
ctx, cancel := context.WithCancel(context.Background())
go func() {
	<-userBailoutSignal
	cancel()
}()

_, err := client.Send(ctx, primitive.SendParams{
	From:     "Support <support@example.com>",
	To:       "alice@example.com",
	Subject:  "Hello",
	BodyText: "Hi there",
})

Distinguishing client-side aborts from API errors#

A canceled or timed-out context surfaces as a standard library sentinel error, while a server response surfaces as *primitive.APIError, so errors.Is and errors.As tell the two apart:

CauseError
Context deadline passedcontext.DeadlineExceeded
cancel() calledcontext.Canceled
Non-2xx or malformed API response*primitive.APIError
  1. 1

    Call the method with a context that can time out or be canceled#

    // Requires: context, time, primitive "github.com/primitivedotdev/sdks/sdk-go"
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    _, err := client.Send(ctx, primitive.SendParams{
    	From:     "Support <support@example.com>",
    	To:       "alice@example.com",
    	Subject:  "Hello",
    	BodyText: "Hi there",
    })
    
  2. 2

    Branch on the error with errors.Is and errors.As#

    // Requires: errors, log, context, primitive "github.com/primitivedotdev/sdks/sdk-go"
    switch {
    case errors.Is(err, context.DeadlineExceeded):
    	log.Println("client-side timeout: the request did not complete in time")
    case errors.Is(err, context.Canceled):
    	log.Println("client-side cancellation: caller aborted the request")
    default:
    	var apiErr *primitive.APIError
    	if errors.As(err, &apiErr) {
    		log.Printf("API error: status=%d code=%s message=%s", apiErr.StatusCode, apiErr.Code, apiErr.Message)
    	} else if err != nil {
    		log.Printf("unexpected error: %v", err)
    	}
    }
    
  3. 3

    Verify the outcome#

    A context.DeadlineExceeded or context.Canceled error means the request may or may not have reached the server, treat the send as indeterminate and retry with an IdempotencyKey rather than assuming it failed. A *primitive.APIError means the server responded, so its StatusCode and Code fields tell you exactly what went wrong.

The one exception: PayEmailChallenge#

X402Client.PayEmailChallenge signs a payment locally with no I/O, so it takes no context.Context argument. Every other x402 and email client method, including Charge, Pay, Send, Reply, and Forward, follows the standard ctx pattern above.

Warning

Idempotency keys (IdempotencyKey on SendParams or ForwardParams) are what make it safe to retry after a canceled or timed-out context. Retrying the same logical send with the same key replays the original response instead of sending duplicate mail, reach for it before you build your own retry loop around context deadlines.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer