Concurrent upload tasks over HTTP/2: request bodies sent strictly sequentially (no stream interleaving) — starved tasks fail behind slow-POST protection

We maintain a large file-sync app. After our upload endpoint moved to HTTP/2, we found that when multiple NSURLSessionUploadTasks run concurrently, all tasks send their request headers immediately (multiplexed on a single connection — confirmed identical localPort via URLSessionTaskMetrics), but request bodies are transmitted essentially one task at a time: while one task's body saturates the uplink, the other tasks send zero body bytes for the entire duration (countOfBytesSent == 0).

This reproduces with both background sessions (BackgroundUploadTask) and default sessions (LocalUploadTask), on Wi-Fi and cellular. iOS 26.5, Xcode 26.3, tasks created with uploadTask(with:fromFile:), multipart POST.

This becomes a hard failure behind a load balancer with slow-POST (RUDY) protection: requests whose first body KB doesn't arrive within 5s are rejected with 408. The starved tasks fail even though the network is healthy. For comparison, OkHttp on Android writes bodies in interleaved 16KB DATA frames under identical conditions, so all streams pass the first-KB check.

Metrics excerpt (4 concurrent uploads, one h2 connection):

task 1: duration 18.7s, sent 180MB (full line rate)
task 2: duration 22.8s, sent 43MB (transmitted only after task 1 finished)
task 3: duration 46.0s, sent 247MB (after task 2)
task 4: duration 5.2s, sent 0 bytes → 408 from the gateway In one run a starved stream sent exactly 65,536 bytes (the default initial stream window) and then stalled.

Questions:

Is this sender-side scheduling (no round-robin between streams' DATA frames) the expected CFNetwork behavior? Does URLSessionTask.priority influence HTTP/2 stream weighting for upload bodies? Is there any other way to influence bandwidth sharing between concurrent uploads? Is there any supported way to opt out of HTTP/2 (constrain ALPN to HTTP/1.1) or cap concurrent streams per connection from the client side? We believe there isn't, but would like to confirm. What is the recommended pattern for concurrent large uploads in this situation?

Filed as FB24062619 with full sanitized metrics attached. Happy to provide more data.

Concurrent upload tasks over HTTP/2: request bodies sent strictly sequentially (no stream interleaving) — starved tasks fail behind slow-POST protection
 
 
Q