Back to News & Insights
Web Development October 7, 2026 · 6 min read

Your video breaks when you serve it from your own server, and 416 is usually the reason

If you moved private files off presigned URLs and started serving them through your own route, you...

Your video breaks when you serve it from your own server, and 416 is usually the reason

If you moved private files off presigned URLs and started serving them through your own route, you have probably hit this: images are fine, PDFs are mostly fine, and video is broken. The player loads, the timeline is dead, and scrubbing does nothing. There is no error in your logs because as far as your server is concerned every request was a 200.

The cause is almost always Range, and specifically what you do with a range you cannot serve. I got this wrong, then I read RFC 9110 properly and measured what a correct implementation actually answers. Here is the table I wish I had had.

When you redirect to a presigned URL, S3 or R2 is answering the request. That includes range requests, and it is very good at them. The moment you proxy instead, that becomes your code, including the parts that have nothing to do with your application.

It is not optional work. A element sends a range request before it will let the user scrub at all. A PDF reader asks for the last few bytes first, because the cross reference table lives at the end of the file. Answer those wrongly and the file looks corrupt rather than forbidden, which is why this shows up as a player bug and not as a permissions bug.

416 Range Not Satisfiable is what you reach for when a range cannot be served as asked. It is almost always the wrong answer.

RFC 9110 says an invalid range must be ignored. Not refused, ignored: you answer 200 with the whole representation, as if the header had not been there. 416 is reserved for a range that parsed cleanly and cannot be satisfied by this particular object, which is a much narrower case than it sounds.

Here is what that means in practice. Twenty six bytes, one per letter of the alphabet, so a wrong offset reads as the wrong letters instead of as plausible binary:

| Range header | status | why | |---|---|---| | bytes=9-4 | 200 | last position before first position, so the range set is invalid | | bytes=-0 | 200 | a suffix of zero asks for nothing | | bytes=abc | 200 | unparseable | | items=0-4 | 200 | a unit that is not bytes | | bytes=0-4,10-14 | 200 | multiple ranges, see below | | bytes=99-120 | 416 | parsed cleanly, past the end of a 26 byte object |

Four of the five that look like a 416 are not one. And the one that is carries something easy to forget:

That */26 tells the client the actual size, which is the information it needs to ask a better question. A 416 without it is a dead end.

bytes=0-4,10-14 is two questions in one header. You have three options and only two of them are safe.

You can answer all of them as multipart/byteranges. You can ignore the header and serve the whole object under a 200. Or you can answer the first one under a 206, which is what a lot of hand written code does because it falls out of the parser naturally.

That third one is a silent data corruption bug. The client asked two questions. Nothing in a 206 says only one of them was answered. So it takes your reply, assumes it covers the first range, and stitches the second range in at an offset that is now wrong. No error anywhere, and a file that is subtly different from the one you stored.

| Range header | status | Content-Range | body | |---|---|---|---| | none | 200 | | abcdefghijklmnopqrstuvwxyz | | bytes=0- | 206 | bytes 0-25/26 | abcdefghijklmnopqrstuvwxyz | | bytes=0-4 | 206 | bytes 0-4/26 | abcde | | bytes=-4 | 206 | bytes 22-25/26 | wxyz | | bytes=20-999 | 206 | bytes 20-25/26 | uvwxyz |

bytes=-4 is a suffix, not a negative offset. It means the last four bytes. If you parse it as "start at -4" you will either throw or serve from the beginning, and the PDF reader that sent it will decide your file is damaged.

bytes=20-999 is not an error. A last position past the end of the object is satisfiable per the spec, and the response covers to the end. Returning 416 here is the second most common mistake after the ones in the first table.

Accept-Ranges: bytes is how a client learns it is allowed to seek. So it has to be on the plain unranged 200, which is the response the client reads before it has ever sent a range. Sending it only on 206 responses tells the client something it already knew, at the only moment it cannot act on it.

About thirty lines. Returns null for anything that must be ignored, and null never means error:

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation