I have an Express server that streams videos to a React app. The Express server is reverse proxied by NGINX. The streaming logic appears to be correct, however, Chrome (and Chromium based browsers) are not properly handling the stream headers and are absolutely flooding my server with requests. FireFox appears to handle it properly requesting a small batch of 8MB chunks and then playing the video before requesting another batch.
Here is my Express endpoint:
app.get("<my-site>/api/video/:encoded", (req, res) => {
const { filePath, fileName } = validationHelper(req.params.encoded)
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
console.warn(`[404] File not found: ${filePath}`)
res.status(404).json({ error: "Video not found" })
return
}
const range = req.headers.range
const fileSize = stats.size
// If there is no range header, send the entire video...
if (!range) {
res.writeHead(200, { "Content-Length": fileSize, "Content-Type": "video/mp4" })
fs.createReadStream(filePath).pipe(res)
return
}
// If there is a range header, send the video in chunks...
const MAX_CHUNK_SIZE = 8 * 1024 * 1024 // 8MB
const parts = range.replace(/bytes=/, "").split("-")
const start = parseInt(parts[0], 10)
let end = parts[1] ? parseInt(parts[1], 10) : start + MAX_CHUNK_SIZE - 1
end = Math.min(end, fileSize - 1)
if (start >= fileSize || end >= fileSize) {
res.status(416).header("Content-Range", `bytes */${fileSize}`).end()
return
}
const contentLength = end - start + 1
const stream = fs.createReadStream(filePath, { start, end })
console.log(JSON.stringify({ range, start, end, contentLength, fileSize }))
res.writeHead(206, {
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": contentLength,
"Content-Type": "video/mp4",
})
stream.pipe(res)
})
})
Here are my NGINX settings:
location /<my-site> {
proxy_pass http://localhost:<port>;
proxy_http_version 1.1;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_set_header If-None-Match $http_if_none_match;
proxy_set_header If-Modified-Since $http_if_modified_since;
# Disable caching for range requests
proxy_cache off;
proxy_buffering off;
proxy_request_buffering off;
proxy_no_cache $http_range;
proxy_cache_bypass $http_range;
# Connection settings
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# Prevent NGINX from modifying range responses
proxy_force_ranges off;
proxy_max_temp_file_size 0;
The HTML is simply:
<video controls preload="metadata" >
<source src={props.src} type="video/mp4" />
</video>
Here is the behavior in FireFox, which is what I'd expect to see. It's requesting several 8MB chunks, then playing through them before making another request:

Chrome on the other hand starts out with a couple 8MB chunks, then just absolutely firehoses my server with requests. The chunks are always out of order and often contain the same byte ranges. The videos total size is roughly 1.3GB, but by the 1 minute mark of the video stream, I've already sent 662 requests and transferred over 3.7GB (it's a 15 minute video...):

Here are sample headers from a Chrome request:
REQUEST:
GET /<my-site>/api/video/<omitted> HTTP/1.1
Accept: */*
Accept-Encoding: identity;q=1, *;q=0
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
DNT: 1
Host: <omitted>
Range: bytes=51740672-
Referer: <omitted>
Sec-Fetch-Dest: video
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site
Sec-Fetch-Storage-Access: active
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
RESPONSE:
HTTP/1.1 206 Partial Content
Server: nginx/1.20.1
Date: Tue, 08 Jul 2025 14:29:31 GMT
Content-Type: video/mp4
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
Accept-Ranges: bytes
Content-Range: bytes 51740672-60129279/1339715678
Content-Length: 8388608
Here are some of the logs that I collected from Chrome:
2025-07-08T10:28:28: {"range":"bytes=0-","start":0,"end":8388607,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:28: {"range":"bytes=1339228160-","start":1339228160,"end":1339715677,"contentLength":487518,"fileSize":1339715678}
2025-07-08T10:28:28: {"range":"bytes=163840-","start":163840,"end":8552447,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:31: {"range":"bytes=3309568-","start":3309568,"end":11698175,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:32: {"range":"bytes=26214400-","start":26214400,"end":34603007,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:37: {"range":"bytes=33914880-","start":33914880,"end":42303487,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:39: {"range":"bytes=6225920-","start":6225920,"end":14614527,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:39: {"range":"bytes=7995392-","start":7995392,"end":16383999,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:39: {"range":"bytes=39288832-","start":39288832,"end":47677439,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:39: {"range":"bytes=12910592-","start":12910592,"end":21299199,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:39: {"range":"bytes=39649280-","start":39649280,"end":48037887,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:39: {"range":"bytes=13172736-","start":13172736,"end":21561343,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=39714816-","start":39714816,"end":48103423,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=13238272-","start":13238272,"end":21626879,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=39911424-","start":39911424,"end":48300031,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=13271040-","start":13271040,"end":21659647,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=40042496-","start":40042496,"end":48431103,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=13336576-","start":13336576,"end":21725183,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=40206336-","start":40206336,"end":48594943,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:40: {"range":"bytes=13434880-","start":13434880,"end":21823487,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=40337408-","start":40337408,"end":48726015,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=13500416-","start":13500416,"end":21889023,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=40468480-","start":40468480,"end":48857087,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=13565952-","start":13565952,"end":21954559,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=40599552-","start":40599552,"end":48988159,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=13664256-","start":13664256,"end":22052863,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:41: {"range":"bytes=40730624-","start":40730624,"end":49119231,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=13729792-","start":13729792,"end":22118399,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=40894464-","start":40894464,"end":49283071,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=13762560-","start":13762560,"end":22151167,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=40992768-","start":40992768,"end":49381375,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=13828096-","start":13828096,"end":22216703,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=41156608-","start":41156608,"end":49545215,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=13926400-","start":13926400,"end":22315007,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:42: {"range":"bytes=41320448-","start":41320448,"end":49709055,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=13959168-","start":13959168,"end":22347775,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=41451520-","start":41451520,"end":49840127,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=14090240-","start":14090240,"end":22478847,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=41615360-","start":41615360,"end":50003967,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=14123008-","start":14123008,"end":22511615,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=41779200-","start":41779200,"end":50167807,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:43: {"range":"bytes=14254080-","start":14254080,"end":22642687,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=41943040-","start":41943040,"end":50331647,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=14286848-","start":14286848,"end":22675455,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=42074112-","start":42074112,"end":50462719,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=14385152-","start":14385152,"end":22773759,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=42172416-","start":42172416,"end":50561023,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=14417920-","start":14417920,"end":22806527,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=42303488-","start":42303488,"end":50692095,"contentLength":8388608,"fileSize":1339715678}
2025-07-08T10:28:44: {"range":"bytes=14483456-","start":14483456,"end":22872063,"contentLength":8388608,"fileSize":1339715678}
I understand that Chrome is aggressive but surely something is wrong here - what are my options?
When a browser makes a request for a range, it's important to give it the range requested. (Unless of course there's some physical reason you can't... in which case your server should reply with 416 Range Not Satisfiable.) Chunking up the response isn't helping the problem.
Even though Chrome is requesting the entire file, it's worth noting that it is still "streaming". Chrome will fill its buffers as fast as possible, but will then put backpressure on the stream until the playback catches up a bit. This backpressure causes the TCP window size to reduce to zero, which effectively pauses the stream from the server until the browser is ready for more. This is how the streaming throttling is done.
When you start to see several requests like this, it often means that the connection is getting closed prematurely. Chrome will happily reconnect to the server and request more data from the point at which it needs it. It does this indefinitely. It's important to make sure Nginx isn't killing the connection too soon, or Chrome will just keep requesting.
This is also common with CDNs, where once the window size closes, they only allow the connection to remain open for a limited amount of time, usually 2 minutes. A couple minutes is long enough, but expect more requests to be made, especially as connection speeds continue to increase.