---
title: "Context, Timeouts, and Cancellation"
canonical: "https://test.abhinandan.one/go-context-timeouts"
markdown_url: "https://test.abhinandan.one/go-context-timeouts.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Go SDK"
description: "Pass context.WithTimeout or context.WithCancel to every Go SDK network call, and use errors.As to tell a client-side abort from a *primitive.APIError."
keywords: ["context.Context", "client.Send Go", "context.WithTimeout", "context.DeadlineExceeded", "primitive.APIError", "PayEmailChallenge no context"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:53.790936+00:00"
source_files:
  - "sdk-go/README.md"
  - "sdk-go/client.go"
sections:
  - {anchor: "per-call-timeout", title: "Per-call timeout"}
  - {anchor: "per-call-cancellation", title: "Per-call cancellation"}
  - {anchor: "distinguishing-client-side-aborts-from-api-errors", title: "Distinguishing client-side aborts from API errors"}
  - {anchor: "step-call-the-method-with-a-context-that-can-time-out-or-be-canceled", title: "Call the method with a context that can time out or be canceled"}
  - {anchor: "step-branch-on-the-error-with-errorsis-and-errorsas", title: "Branch on the error with errors.Is and errors.As"}
  - {anchor: "step-verify-the-outcome", title: "Verify the outcome"}
  - {anchor: "the-one-exception-payemailchallenge", title: "The one exception: PayEmailChallenge"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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()`.

```go
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](https://test.abhinandan.one/email-model.md) (`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.

```go
// 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:

| Cause | Error |
|---|---|
| Context deadline passed | `context.DeadlineExceeded` |
| `cancel()` called | `context.Canceled` |
| Non-2xx or malformed API response | `*primitive.APIError` |

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

```go
// 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. Branch on the error with errors.Is and errors.As

```go
// 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. 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`](https://test.abhinandan.one/go-sending-emails.md) 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.
