Understanding HTTP 100 Continue: The Handshake Before the Upload
100 Continue is the only status code most developers never see and almost never send — yet it's quietly running underneath every large file upload your API accepts. It isn't the server confirming your request succeeded (that's still coming), and it isn't the server saying it's done reading anything. It's a server saying "keep going" before the client has finished sending. This post covers the Expect: 100-continue negotiation that triggers it, why curl sends it automatically on big uploads, what happens when a server doesn't understand it, and where it fits next to 101 and the rest of the 1xx family.
What Is a 100?
A 100 is an interim response — it's not the final answer to a request, it's a checkpoint the server sends partway through receiving one.
The 100 (Continue) status code indicates that the initial part of a request has been received and has not yet been rejected by the server. The server intends to send a final response after the request has been fully received and acted upon. — RFC 9110, Section 15.2.1
In plain English: the client sent the request line and headers, the server looked them over and found nothing objectionable yet, and it's telling the client "go ahead and send the body." A final response — 200, 201, 400, whatever the request actually earns — still follows once the server has the whole thing.
Don't confuse a 100 with request completion. It's the server saying "continue sending," not "I'm finished processing." You'll still get a real final status code after the full request is received and acted upon.
The Negotiation: Expect: 100-continue
100 only shows up when the client explicitly asks for it, via the Expect request header:
A "100-continue" expectation informs recipients that the client is about to send (presumably large) content in this request and wishes to receive a 100 (Continue) interim response if the method, target URI, and header fields are not sufficient to cause an immediate success, redirect, or error response. This allows the client to wait for an indication that it is worthwhile to send the content before actually doing so, which can improve efficiency when the data is huge or when the client anticipates that an error is likely (e.g., when sending a state-changing method, for the first time, without previously verified authentication credentials). — RFC 9110, Section 10.1.1
Walk through what that buys you. Without Expect: 100-continue, a client uploading a 2GB video has to push the entire 2GB before finding out the request was going to be rejected anyway — bad auth token, wrong Content-Type, a resource that doesn't accept PUT. With it, the exchange looks like this:
PUT /uploads/video.mp4 HTTP/1.1
Host: api.example.com
Content-Type: video/mp4
Content-Length: 2147483648
Expect: 100-continue
The client sends only the request line and headers, then pauses. If the server is happy with what it sees, it answers with a bare 100 Continue and the client streams the body. If the server already knows it's going to reject the request — say, the Authorization header is missing — it can send 401 or 405 right there, and the client never wastes the bandwidth.
Servers Aren't Required to Wait For You
Two things temper how reliable this negotiation is in practice, and both come straight from the RFC's requirements:
- The client isn't required to wait indefinitely. A client that sends a 100-continue expectation "is not required to wait for any specific length of time; such a client MAY proceed to send the content even if it has not yet received a response... [and] SHOULD NOT wait for an indefinite period before sending the content" (RFC 9110 §10.1.1). In other words, a slow or silent server doesn't get to stall the client forever — after some timeout, the client just sends the body anyway.
- The server can skip the 100 entirely. A server "MAY omit sending a 100 (Continue) response if it has already received some or all of the content for the corresponding request, or if the framing indicates that there is no content" (same section). If your server doesn't implement
Expecthandling at all, it isn't violating the spec by ignoring the header — it just won't get the early-rejection benefit.
That combination — client won't wait forever, server can ignore the header — is why Expect: 100-continue behaves more like a polite request than a guarantee.
curl Sends This Automatically (and Times Out on Its Own)
If you've ever seen a mysterious one-second pause on a curl -X PUT upload, this is why. curl adds Expect: 100-continue automatically on POST/PUT requests whose body is "known or suspected to be larger than one megabyte." It then waits up to 1000 milliseconds for a 100 response before sending the body regardless — tunable with --expect100-timeout <seconds>, or removable entirely with -H Expect: if you want to skip the negotiation altogether.
# Default: curl adds Expect: 100-continue for bodies over ~1MB,
# waits up to 1s for a 100 response, then sends the body anyway.
curl -X PUT --data-binary @video.mp4 https://api.example.com/uploads/video.mp4
# Give a slow server more time to respond with 100 (or a rejection)
curl --expect100-timeout 5 -X PUT --data-binary @video.mp4 https://api.example.com/uploads/video.mp4
# Skip the negotiation entirely
curl -H 'Expect:' -X PUT --data-binary @video.mp4 https://api.example.com/uploads/video.mp4Handling It on the Server
Most frameworks handle this transparently — you don't write code that returns a 100 by hand, because the underlying HTTP server library answers it before your route handler even runs.
Node.js (raw http server)
Node's http.Server emits a checkContinue event specifically for this. If nothing listens for it, Node auto-responds with 100 Continue on your behalf:
import http from "node:http";
const server = http.createServer();
// Only needed if you want to inspect the request before committing
// to receive the body — e.g., reject unauthenticated uploads early.
server.on("checkContinue", (req, res) => {
if (!req.headers.authorization) {
res.writeHead(401);
res.end();
return;
}
res.writeContinue();
server.emit("request", req, res);
});
server.listen(3000);Without a checkContinue listener at all, Node just sends the 100 automatically and emits the normal request event — which is why Express apps and Next.js route handlers don't need to think about this: the framework's underlying http.Server already answered before your code sees the request.
NGINX / reverse proxies
NGINX handles Expect: 100-continue transparently when proxying to an upstream — it doesn't require configuration for the common case. If you're running a custom TCP proxy or load balancer in front of your app, verify it forwards the 100 response rather than buffering it; a proxy that swallows interim responses defeats the whole point of the negotiation.
Common Pitfalls
- Treating 100 as if it were the final response. Client code that stops after seeing a
100and never reads the real status line will hang forever waiting for a response it already (mistakenly) consumed. A100is never the last thing on the wire for that request. - Assuming every server honors
Expect: 100-continue. Plenty of servers and proxies just ignore the header and read the body immediately — spec-compliant, since the RFC says a server MAY omit the interim response. Don't build upload logic that depends on receiving a100before it will send data; treat it as an optimization, not a contract. - Waiting indefinitely for a 100 that isn't coming. Per the RFC, clients aren't supposed to block forever on this — mirror that in your own HTTP client code. If you hand-roll a client that implements
Expect: 100-continue, give it a timeout (curl's default is 1 second) and send the body anyway once it expires. - Debugging a "hung" upload without checking for a stuck 100 exchange. If a large upload stalls for exactly one second before continuing, that's very likely curl's
Expect: 100-continuetimeout — not a network problem.
Wrapping Up
100 Continue is a small, self-limiting optimization: a way for a client to ask "is this worth sending?" before it commits to sending a potentially huge body, with built-in escape hatches on both ends so nobody's stuck waiting forever. The rules of thumb:
- 100 means "keep sending" — it's not a final response, and it's not confirmation of success
- It only appears when the client sends
Expect: 100-continue, almost always paired with large request bodies - Servers are allowed to skip it; clients aren't required to wait for it indefinitely
- curl adds it automatically above roughly 1MB and gives up waiting after 1 second by default
For more, see our page on 100 Continue, and check out 101 Switching Protocols for the other 1xx code most developers actually encounter, or browse the rest of the 1xx informational codes.