Go: a context timeout stops waiting on a Sume job, not the job
context.WithTimeout cancels your status read, never the generation. A 29-line Go loop shows the DeadlineExceeded branch, and why the job id must be stored.

A context.WithTimeout around your status calls ends your wait; the Sume job carries on and bills. Sume's docs say it plainly: a client-side timeout does not cancel the job, so you stopped the wait and nothing else. Store the job id before you wait, and on a deadline error log it and poll again later.
Three ways a wait can end
Keep the ending of the wait apart from the ending of the job. Only the last row below changes what Sume does.
| Your side | The job | Next step |
|---|---|---|
| Context deadline passes | Keeps running and bills | Poll again with the stored id |
| Process restarts | Keeps running and bills | Resume from the stored id |
| terminal is true | Done | Fetch the result if completed |
| POST /cancel | Canceled only if generation has not started | Read the 409 job_generation_already_started otherwise |
The loop
The 3-second deadline in the sample is for demonstration; a video job needs minutes, and the docs call 20 minutes a reasonable client deadline for video. select waits for either the poll delay or the deadline, so the loop never sleeps past the context.
package main
import ("context"; "encoding/json"; "errors"; "fmt"; "net/http"; "os"; "time")
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // your patience, not the job's
defer cancel()
for {
req, _ := http.NewRequestWithContext(ctx, "GET", os.Getenv("SUME_BASE")+"/v1/jobs/"+os.Args[1]+"/status", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SUME_API_KEY"))
res, err := http.DefaultClient.Do(req)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("stopped waiting; the job keeps running and billing. Store its id.")
return
} else if err != nil {
fmt.Println(err)
return
}
var s struct{ Data struct{ Status string `json:"sume_status"`; Terminal bool } }
json.NewDecoder(res.Body).Decode(&s)
res.Body.Close()
if s.Data.Terminal {
fmt.Println(s.Data.Status)
return
}
select {
case <-time.After(2 * time.Second):
case <-ctx.Done():
}
}
}What it leaves undone
It does not cancel. If you want the job gone, call POST /v1/jobs/{id}/cancel, which succeeds only before generation starts and answers 409 afterwards. Cancelling a job that is already canceled returns the same job.
It also does not read next_poll_after_seconds; the fixed 2-second delay is fine for a demo and wrong for a long queue, where the server's hint is a minimum.
Sources
Related posts
More in Developers
- Go client for a Sume bulk queue: transient polls and exit status
Create a Sume bulk queue from Go, poll the status_url with a doubling gap, treat 429 and 503 as transient, and exit 1 on failed items. Standard library only.
- Go httptest: a four-case table test for a Sume webhook handler
Sign a fixture body with sume-v1 HMAC, then assert 204 for a valid call and 401 for a stale timestamp, an empty server secret and a wrong secret.
- Go: poll a Sume job and stop on any terminal state, even a new one
A Go status loop that names completed, failed and canceled, stops on terminal=true for anything else, and never sleeps less than next_poll_after_seconds.
- gpt-image-1.5 is removed Dec 1: a 55-day cutover calendar
OpenAI removes gpt-image-1.5, gpt-image-1-mini and chatgpt-image-latest on December 1, 2026. A week-by-week plan from October 7 to move to GPT Image 2.5.
Written by Sume