skip to content

Uploads made with curl consistently stall for about a second before the request body is sent, and the stall disappears when the request goes direct instead of through a proxy. What is curl doing, and how would you confirm and remove the delay?

level: seniorimportance: should knowfreq 30%

answer

  1. curl auto-adds Expect over ~1 KB
  2. No 100 → wait 1s → send anyway
  3. Flat delay, size-independent
  4. curl -v --trace-time to locate the gap
  5. -H 'Expect:' removes it

basics

~20 s

curl adds Expect: 100-continue for bodies over about 1 KB and waits up to a second for 100 Continue. The proxy never sends it, so curl waits out the timer then uploads. Confirm with verbose tracing; remove it by sending an empty Expect header.

solid answer

~50 s

curl attaches `Expect: 100-continue` automatically once the body exceeds roughly 1 KB, then withholds the body waiting for an interim `100 Continue`. If the peer never sends one — a proxy that strips `Expect`, swallows 1xx responses, or simply blocks reading the body — curl waits out its one-second timer and sends the body anyway. Nothing fails; every upload just costs an extra second. Confirm it on the wire: `curl -v` shows the request headers including `Expect: 100-continue` and whether a `< HTTP/1.1 100 Continue` line ever comes back; `--trace-time` timestamps the gap so you can see the pause sitting between the headers and the body rather than in DNS, TLS or server processing. A packet capture shows the same shape. Remove it with `-H 'Expect:'`, which sends the header empty and makes curl omit it. In library clients, disable expect-continue or shorten its timeout. Longer term, fix the proxy so it relays interim responses.

code

bash · 2 lines
bash
curl -v --trace-time -X PUT --data-binary @big.bin https://proxy.example.com/upload
curl -v --trace-time -X PUT --data-binary @big.bin -H 'Expect:' https://proxy.example.com/upload

go deeper

for a junior

Know that curl adds the header by itself for larger bodies and waits about a second when nobody answers.

for a middle

Explain the fallback timer, locate the gap with verbose timestamped output, and remove the header with the empty-header idiom.

for a senior

Enumerate the proxy behaviours that produce silence, prove where the header was lost, and choose between disabling, retuning the timeout and fixing the intermediary.

for a principal

Set a client policy by payload profile and network path, and treat 'optional optimization silently degrading to fixed latency' as a class of defect worth monitoring for, not just this one instance.

## The symptom Uploads work, results are correct, but every one takes about a second longer than it should — and the extra second does not scale with body size, which immediately rules out bandwidth. Small requests are unaffected. It appears only on one network path. ## What curl does by default curl adds `Expect: 100-continue` on its own when it is sending a body larger than about 1 KB (`PUT`, `POST` with a file, `--data-binary` of any size past the threshold). It then writes headers only and waits for the interim response before writing the body. If no response arrives within its internal expect-100 timeout — one second — curl gives up waiting and sends the body regardless. So the fixed one-second penalty is not a bug; it is the specified fallback doing its job against a peer that never answers. ## Why the proxy changes the outcome Intermediaries handle the expectation in incompatible ways: - Some **strip** the `Expect` header before forwarding, so the origin never sees it and never sends `100 Continue`; the proxy itself sends nothing either. - Some **drop 1xx responses** on the way back, because informational responses are easy to overlook when a proxy is written to relay one status line per request. The origin behaved correctly and curl still sees silence. - Some **buffer the whole request** before contacting the origin, which means they must read the body — so they wait for bytes while curl waits for a status. - Some **answer `100 Continue` themselves**, which removes the delay but also removes the benefit: the client uploads the full body to the proxy even if the origin will reject it. - A few legacy ones answer `417 Expectation Failed`, which costs a retry. Going direct to an origin that implements the mechanism produces an immediate `100 Continue` and no delay — exactly the difference described. ## Confirming it 1. `curl -v --trace-time` on the failing path. Look for `> Expect: 100-continue` in the request and for a `< HTTP/1.1 100 Continue` line. The timestamps place the gap precisely between headers and body — not in name resolution, connection setup or server processing. 2. Re-run with `-H 'Expect:'`. If the second disappears, the diagnosis is confirmed in one step. 3. If you need proof of who dropped what, capture packets or read proxy access logs; a proxy that strips the header will show a request to the origin without `Expect`. 4. Compare direct versus proxied with identical bodies to isolate the path. Be careful not to misattribute: a flat, size-independent delay before the first payload byte is the signature. If the delay scales with body size it is bandwidth; if it sits before the request line it is DNS, TCP or TLS; if it sits after the body it is server processing. ## Fixing it - **Client-side, immediate:** `curl -H 'Expect:'` sends the header with an empty value, which is curl's idiom for removing a header it would otherwise add. For scripted uploads this is the usual fix. - **Library clients:** Java's `HttpClient.Builder.expectContinue(false)`, Go's `Transport.ExpectContinueTimeout` (set small, or disable by not sending the header), Apache HttpClient's request config, urllib3/requests which do not send it by default at all. Shortening the timeout instead of disabling keeps the benefit when the peer does support it. - **Infrastructure:** make the proxy relay interim responses, or at least stop stripping `Expect`. This is the correct fix if large uploads genuinely benefit from early rejection. - **Decide deliberately:** if bodies are small, disable the expectation everywhere — it can only cost. If bodies are large and rejection is plausible (auth, quota, size limits), keep it and fix the path. ## The wider lesson An optional protocol optimization that depends on every hop understanding it will, on some paths, degrade into a fixed latency tax. The tell is a constant delay that does not scale with payload, and the debugging move is to compare paths while watching the wire rather than the application log.

  • How do you tell this apart from a slow server or a slow network?
    The delay is flat and independent of body size, which excludes bandwidth, and timestamped tracing places it between the request headers and the first body byte rather than before the request line or after the body. Sending the same request with the Expect header suppressed makes it vanish, which confirms it in one test.
  • When is disabling expect-continue the wrong fix?
    When bodies are genuinely large and the server rejects a meaningful share of them on headers alone — unauthorized, oversized, wrong media type. Disabling then trades a one-second wait for full transfers of payloads that will be thrown away. The better fix is to make the intermediary relay interim responses, or to lower the timeout instead of removing the header.

saying these in an interview costs you the question

  • Blaming the server or the network for a delay that is a client-side default
  • Assuming curl only sends Expect when the user asks for it
  • Trying to remove the header with -H 'Expect: ' expecting a value rather than the empty-header idiom
  • Concluding the upload failed — it succeeds, it is only slower
  • Disabling expect-continue globally without checking whether large uploads rely on early rejection

context