{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/go-context-timeouts","markdown_url":"https://test.abhinandan.one/go-context-timeouts.md","article":{"id":"a27851ed-30fd-4ad9-b2cf-8323e0d4d947","article_slug":"go-context-timeouts","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:53.790936+00:00","keywords":["context.Context","client.Send Go","context.WithTimeout","context.DeadlineExceeded","primitive.APIError","PayEmailChallenge no context"],"meta_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.","og_image_url":null,"source_file_paths":["sdk-go/README.md","sdk-go/client.go"],"recording_id":null,"replayable":false,"task_name":"Context, Timeouts, and Cancellation","category":"Go SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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.\n\nYou 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`.\n\n## Per-call timeout\n\nWrap the call in `context.WithTimeout` and always `defer cancel()`.\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nfunc main() {\n\tclient, err := primitive.NewClient(\"prim_test\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Cancel after 15 seconds.\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\t_, err = client.Send(ctx, primitive.SendParams{\n\t\tFrom:     \"Support <support@example.com>\",\n\t\tTo:       \"alice@example.com\",\n\t\tSubject:  \"Hello\",\n\t\tBodyText: \"Hi there\",\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"send failed: %v\", err)\n\t}\n}\n```\n\n<Tip>\n\nFor [wait mode](email-model) (`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.\n\n</Tip>\n\n## Per-call cancellation\n\nUse `context.WithCancel` and call `cancel()` from wherever the abort signal arrives, for example a user closing a connection or your own shutdown handler.\n\n```go\n// Requires: context, primitive \"github.com/primitivedotdev/sdks/sdk-go\"\nctx, cancel := context.WithCancel(context.Background())\ngo func() {\n\t<-userBailoutSignal\n\tcancel()\n}()\n\n_, err := client.Send(ctx, primitive.SendParams{\n\tFrom:     \"Support <support@example.com>\",\n\tTo:       \"alice@example.com\",\n\tSubject:  \"Hello\",\n\tBodyText: \"Hi there\",\n})\n```\n\n## Distinguishing client-side aborts from API errors\n\nA 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:\n\n| Cause | Error |\n|---|---|\n| Context deadline passed | `context.DeadlineExceeded` |\n| `cancel()` called | `context.Canceled` |\n| Non-2xx or malformed API response | `*primitive.APIError` |\n\n<Steps>\n\n<Step title=\"Call the method with a context that can time out or be canceled\">\n\n```go\n// Requires: context, time, primitive \"github.com/primitivedotdev/sdks/sdk-go\"\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\n\n_, err := client.Send(ctx, primitive.SendParams{\n\tFrom:     \"Support <support@example.com>\",\n\tTo:       \"alice@example.com\",\n\tSubject:  \"Hello\",\n\tBodyText: \"Hi there\",\n})\n```\n\n</Step>\n\n<Step title=\"Branch on the error with errors.Is and errors.As\">\n\n```go\n// Requires: errors, log, context, primitive \"github.com/primitivedotdev/sdks/sdk-go\"\nswitch {\ncase errors.Is(err, context.DeadlineExceeded):\n\tlog.Println(\"client-side timeout: the request did not complete in time\")\ncase errors.Is(err, context.Canceled):\n\tlog.Println(\"client-side cancellation: caller aborted the request\")\ndefault:\n\tvar apiErr *primitive.APIError\n\tif errors.As(err, &apiErr) {\n\t\tlog.Printf(\"API error: status=%d code=%s message=%s\", apiErr.StatusCode, apiErr.Code, apiErr.Message)\n\t} else if err != nil {\n\t\tlog.Printf(\"unexpected error: %v\", err)\n\t}\n}\n```\n\n</Step>\n\n<Step title=\"Verify the outcome\">\n\nA `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`](go-sending-emails) rather than assuming it failed. A `*primitive.APIError` means the server responded, so its `StatusCode` and `Code` fields tell you exactly what went wrong.\n\n</Step>\n\n</Steps>\n\n## The one exception: PayEmailChallenge\n\n`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.\n\n<Warning>\n\nIdempotency 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.\n\n</Warning>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Sending Emails\" href=\"go-sending-emails\">\n\nControl Wait/WaitTimeoutMs delivery semantics and dedupe retries with IdempotencyKey.\n\n</Card>\n\n<Card title=\"Client and Configuration\" href=\"go-client-configuration\">\n\nUnderstand how NewClient and NewClientWithOptions construct the dual-host Go client.\n\n</Card>\n\n<Card title=\"Error Handling\" href=\"go-error-handling\">\n\nInspect APIError and the webhook/x402 error types to handle failures precisely.\n\n</Card>\n\n<Card title=\"x402 Errors\" href=\"go-x402-errors\">\n\nInterpret X402Error status codes and indeterminate-outcome cases, including a Status: 0 Pay() failure.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-go/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Context%2C+Timeouts%2C+and+Cancellation&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fgo-context-timeouts","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}