Go http.Client Timeout covers the body: Sume job poll
Go's Client.Timeout includes redirects and reading the body and keeps running after Do returns. Use it per poll and put the job deadline on a context.

Short answer
Set http.Client.Timeout to the length of one status request, not the length of the job. The net/http documentation says the timeout covers connection time, any redirects and reading the response body, and that the timer keeps running after Do returns. Put the overall job deadline on a context instead.
Two clocks, two jobs
A Sume job can run for minutes, but each status read is a short GET. Mixing the two into one client-wide timeout either kills long jobs or lets a stalled read hang. Keep them apart: a per-request budget on the client and a whole-loop budget on the context.
The Go docs also recommend NewRequestWithContext with Client.Do when you want a specific context on a request, which is exactly the shape a poll loop needs.
| Mechanism | Bounds |
|---|---|
| Client.Timeout | Connect, redirects and reading Response.Body, for one request |
| Context passed to NewRequestWithContext | Whatever deadline or cancel you attach, across the request |
| Loop deadline in your code | The whole poll, across many requests |
What to poll
Poll GET /v1/jobs/:id/status. The response carries terminal, sume_status and next_poll_after_seconds. Stop when terminal is true, and sleep for next_poll_after_seconds when it is present. Statuses are queued, processing, completed, failed and canceled, and the last three are terminal.
When the deadline fires, the job is not canceled. A Go timeout only stops your reads, and the work in flight is still paid for. Read the result later, or call cancel explicitly, which works only before generation starts. Do not submit the same request again; see Jobs and results.
package main
import ("context"; "encoding/json"; "fmt"; "net/http"; "os"; "time")
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
hc := &http.Client{Timeout: 15 * time.Second}
url := "https://api.sume.com/v1/jobs/" + os.Args[1] + "/status"
for {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("x-api-key", os.Getenv("SUME_API_KEY"))
res, err := hc.Do(req)
if err != nil { fmt.Println("poll failed:", err); return }
var s struct { Terminal bool `json:"terminal"`; Next float64 `json:"next_poll_after_seconds"` }
json.NewDecoder(res.Body).Decode(&s)
res.Body.Close()
fmt.Println(res.StatusCode, s.Terminal)
if res.StatusCode != 200 || s.Terminal { return }
select {
case <-ctx.Done(): return
case <-time.After(time.Duration(max(s.Next, 3) * float64(time.Second))):
}
}
}
Details that bite
Read the body before you return from the function that owns the response. The Client timer interrupts body reads, so a slow download of a large result file can fail well after the headers arrived. For result downloads, use a separate client with a longer timeout.
Send one credential only. The Sume API rejects a request carrying both Authorization and x-api-key with 401 unauthorized, so the sample sets x-api-key and nothing else.
Sizing the two budgets
There is no universal number, so derive both from your own traffic. For the client timeout, look at how long a status read takes in your region and leave generous headroom; the docs treat each read as a short call. For the context deadline, use the longest job you are willing to wait on in this process, and let a separate worker or a webhook pick up anything slower.
Reads are cheap against the rate limit. The API gives each key a read budget that is 40 times its write budget, so a poll loop that honors next_poll_after_seconds stays far from the cap. The math is in the polling and read budget post.
If you would rather not poll at all, submit with a webhook and let the terminal callback arrive; keep a slow poll as a backup. Either way, the Go timeout you set is about your reads, never about the job.
Error handling in the loop
The sample exits on any non-200 status. In production, branch on the code. A 429 means slow down and use the retry-after value; a 404 not_found for a job id usually means the key belongs to a different member than the one that created the job, because job reads are scoped to the key's own member. A 401 means the credential is wrong or two were sent. None of these should be retried blindly in a tight loop.
Sources
Related posts
More in Developers
- Google Images formats and filenames for Sume output
Google Search supports BMP, GIF, JPEG, PNG, WebP, SVG and AVIF in img src, and wants short filenames and real alt text. Rename Sume downloads first.
- Google Play app icon: 512x512 32-bit PNG with alpha, 1024 KB
Play wants a 512 x 512, 32-bit PNG with alpha, at most 1024 KB. Get a transparent square from ChatGPT Image 2.5 on Sume, then resize and check the size.
- Google Play screenshots: 9:16 at 1080x1920, side limit 2x
Play screenshots need 320 to 3840 px and a long side at most twice the short side. Four 1080 x 1920 9:16 shots also meet the promo eligibility note on the page.
- Goose 1.52 recipe consent before extensions: a Sume MCP recipe
Goose 1.52 asks for recipe consent before session/new spawns extensions, and caps recipe size. What to put in a recipe that uses Sume's MCP server.
Written by Sume