Go 1.27 drains response bodies: a Sume job poll loop

Go 1.27 drains unread HTTP/1 body bytes on Close. A stdlib loop that polls GET /v1/jobs/:id/status and honors next_poll_after_seconds.

5 min readSume
All posts

Short answer

Poll GET /v1/jobs/:id/status in a plain net/http loop, always decode or close the body, sleep for next_poll_after_seconds, and stop when terminal is true. The Go 1.27 release notes say an HTTP/1 Response.Body now drains unread content when it is closed, up to a conservative limit, to allow better connection reuse, so a loop that closes every response keeps its connection warm.

That matters for polling because a video job can take minutes and the loop issues hundreds of tiny GETs. Sume's jobs and results guide describes the contract: every submit returns a durable job id, async is the default mode, and the status endpoint tells you when to look again.

What changed in Go 1.27 that touches a poll client

The release notes page also lists server-side items. They are worth knowing if the same binary both polls Sume and receives webhooks, but only the body-drain note changes client behavior.

Go 1.27 net/http and testing notes (read 2026-10-03)
ItemWhat the notes say
HTTP/1 Response.Bodydrains unread content on Close, up to a conservative limit, for better connection reuse
HTTP/2 server priorityserver accepts client priority signals per RFC 9218; Server.DisableClientPriority restores round-robin
Server.MaxHeaderValueCountnew field that caps how many header values a server accepts
testing/synctest.Sleepnew helper combining time.Sleep and synctest.Wait

The envelope the loop reads

Every status read returns a data object. The loop below needs only four fields from it, and it never builds a URL by hand beyond the job id you were given at submit time.

Sume documents the same loop as pseudocode: poll, call a progress callback, stop on terminal, then fetch the result only when the job completed. A 409 job_not_completed from the result route means you asked too early; read the failure off the job record instead.

Fields on GET /v1/jobs/:id/status that a poll loop reads (as of 2026-10-03)
FieldWhat it tells the loop
terminaltrue once the job is completed, failed or canceled; stop polling
result_readytrue when GET /v1/jobs/:id/result will return the result
sume_statusqueued, processing, completed, failed or canceled
next_poll_after_secondshow long to wait before the next read; null once terminal
status_url, result_url, events_url, cancel_urlabsolute URLs the envelope hands back, so you never build paths by hand

A stdlib poll loop

Save the file as poll.go and run SUME_API_KEY=... go run poll.go job_123. The key goes in Authorization: Bearer, which the docs use in every curl example. The loop reads from the read budget, which on the Free plan is 4,800 requests per minute against 120 writes, so a two-second cadence is nowhere near it.

package main

import ("encoding/json"; "fmt"; "net/http"; "os"; "time")

func main() {
	url := "https://api.sume.com/v1/jobs/" + os.Args[1] + "/status"
	for {
		req, _ := http.NewRequest("GET", url, nil)
		req.Header.Set("Authorization", "Bearer "+os.Getenv("SUME_API_KEY"))
		res, err := http.DefaultClient.Do(req)
		if err != nil { fmt.Println(err); os.Exit(1) }
		var s struct{ Data struct {
			Terminal bool `json:"terminal"`
			Status string `json:"sume_status"`
			Next *float64 `json:"next_poll_after_seconds"`
		} }
		err = json.NewDecoder(res.Body).Decode(&s)
		res.Body.Close()
		if err != nil || res.StatusCode != 200 { fmt.Println("status", res.StatusCode, err); os.Exit(1) }
		if s.Data.Terminal { fmt.Println(s.Data.Status); return }
		wait := 2.0
		if s.Data.Next != nil { wait = *s.Data.Next }
		time.Sleep(time.Duration(wait * float64(time.Second)))
	}
}

What Sume does and does not do

Sume gives every submit a job id in the first response, whichever mode you chose, and keeps the job running if your process dies. It does not push progress: the Developer API has no SSE or WebSocket transport, and GET /v1/jobs/:id/events is a pull snapshot.

A client-side timeout does not cancel anything. If your deadline passes, the job still runs and still bills; store the id and resume from status_url, or call POST /v1/jobs/:id/cancel, which only succeeds before generation starts and otherwise answers 409 job_generation_already_started.

  • Never resubmit a paid request because the poll loop timed out; retry the submit only with the same Idempotency-Key.
  • Use webhook mode when you would rather be told, and keep this loop as the backup.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume