Go WaitGroup fan-out for Wan 3.0 submits: no mutex, clean -race
Submit 12 Wan 3.0 clips from Go with sync.WaitGroup and a 4-slot channel. Each goroutine owns one slice index, so no mutex. Runs clean under go run -race.

The usual Go fan-out advice is to guard a results map with a mutex. You do not need one here. Allocate the results slice up front, give each goroutine exactly one index, and only that goroutine ever writes to it. The race detector has nothing to flag, and wg.Wait() is the only synchronization left.
The program below posts 12 five-second Wan 3.0 clips at 480p to POST /v1/videos. A buffered channel of 4 keeps at most four requests in flight. Every shot gets its own Idempotency-Key, so rerunning the whole program after a crash replays the original jobs instead of creating duplicates. The sample ran against a local stand-in server that answers 202, with go run -race, on Go 1.27.1; it was not pointed at the live API.
The code (29 lines, Go 1.22 or later)
Go 1.22 made the loop variable i per-iteration, which is why the goroutine can read it directly. On older Go versions, pass i in as an argument. Set SUME_BASE=https://api.sume.com and SUME_API_KEY first, then go run fan.go.
package main
import ("bytes"; "fmt"; "net/http"; "os"; "sync")
func submit(base, key string, i int) int {
body := fmt.Sprintf(`{"model":"wan-3.0","prompt":"shot %d","duration":5,"resolution":"480p"}`, i)
req, _ := http.NewRequest("POST", base+"/v1/videos", bytes.NewBufferString(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
req.Header.Set("Idempotency-Key", fmt.Sprintf("trailer-v3-shot-%02d", i))
res, err := http.DefaultClient.Do(req)
if err != nil { return 0 }
defer res.Body.Close()
return res.StatusCode
}
func main() {
base, key := os.Getenv("SUME_BASE"), os.Getenv("SUME_API_KEY")
codes := make([]int, 12)
sem := make(chan struct{}, 4) // in-flight budget
var wg sync.WaitGroup
for i := range codes {
wg.Add(1); sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
codes[i] = submit(base, key, i)
}()
}
wg.Wait()
fmt.Println(codes)
}Where the 4 comes from
The channel width is your pacing choice, not a Sume limit. Sume admits a paid job as queued even when every processing seat is busy, and only answers 429 queue_full when the accepted capacity is used up. A sensible width is the in-flight budget from the docs: max(0, concurrency_limit - active - queued), capped at queue_capacity_remaining. For an idle Pro workspace that is 4.
Do not size the channel from wave_size_hint. The docs call it a submission-wave hint only: max(1, floor(queue_capacity_remaining * 0.75)), which is 18 on an idle Pro workspace.
| Plan | Processing | Queue (default) | Accepted |
|---|---|---|---|
| Free | 1 | 5 | 6 |
| Pro | 4 | 20 | 24 |
| Startup | 8 | 40 | 48 |
| Scale | 20 | 100 | 120 |
What WaitGroup does not wait for
wg.Wait() returns when every submit call has an answer. The videos are not done at that point; each 202 response carries an id and a polling_url, and the job is pending until it is picked up. Poll GET /v1/videos/{id} afterwards (statuses pending, in_progress, completed, failed, cancelled), or pass a callback_url and verify the signed webhook instead.
The result slice holds HTTP status codes, with 0 for a transport error. A 429 or 503 in that slice is the cue to resubmit that shot with the same key, not a new one.
Sources
Related posts
More in Developers
- 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.
- GPT Image 2.5 banner: 3840x1280 passes the 3:1 rule
3840x1280 is exactly 3:1, both edges are multiples of 16 and it is 4,915,200 pixels, so Sume accepts it on GPT Image 2.5. The four checks shown.
- Grok Imagine returns one image per call: fan out 1,000 in Python
Grok Imagine on Sume takes n=1, so 1,000 images means 1,000 requests at $0.025 each, $25.00 in all. A short Python fan-out shows how to run them safely.
- Handle every Sume API error with one switch on next_action
Sume errors share one envelope. Branch on next_action, retryable and retry_after_seconds, and your client handles new codes without a code change. JS sample.
Written by Sume