> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# The Presigned URL That Expired Mid-Upload
- URL: https://debugly.dev/presigned-url-expired-mid-upload/
- Published: 2026-10-09T16:49:00.000Z
- Updated: 2026-10-17T05:55:46.000Z
- Description: The upload took longer than the signature window, so the final chunk was rejected with a 403, and the client reported a network error rather than an expiry…
- Author: Rohit Bhadani
- Tags: Error Autopsy, Storage, Networking

I would like to say the incident was the problem, because that would be a shorter post, but it started at the failed.

Here is the shape of the presigned URL that outlives its signature, and the one property that makes it a client-side mystery: the rejection arrives as an authorization failure at the end of a long transfer, so it looks like a network error rather than a clock problem.

This was an S3 multipart upload, and the mechanics are the same for any time-limited signed URL used for a transfer of variable duration.

## Why the window was too short

The signature was the fifteen minutes, and the fifteen was the default, and the default was the reasonable for the small file, and the small file was the tested, and the tested was the pass, and the pass was the deploy, and the deploy was the large file, and the large file was the slow connection, and the slow connection was the twenty minutes, and the twenty was the beyond, and the beyond was the expiry, and the expiry was the 403.

The window is the fixed and the upload is the variable, and the variable is the size divided by the bandwidth, and the bandwidth is the customer's, and the customer's is the unknown, and the unknown is the window's inadequacy, and the inadequacy is the incident, and the incident is the reason the window should be derived from the expected transfer rather than set to a default.

The signature is the grant's expiry, and the grant's is the security's, and the security's is the short, and the short is the correct instinct, and the instinct is the trade, and the trade is the long upload's failure, and the failure is the fix's target, and the target is the multipart with the per-part signature.

## Why the error was misread

The client saw the 403, and the 403 was the forbidden, and the forbidden was the permission, and the permission was the credentials, and the credentials were the checked, and the checked were the fine, and the fine was the confusion, because the credentials were valid and the signature was expired, and the two are the different failures with the same status.

The error body said the request had expired, and the body was the answer, and the answer was the not logged, and the not logged was the client's handling, and the handling was the generic, and the generic was the network error, and the network error was the misleading, and the misleading was the hour, and the hour was the credentials' audit, and the audit was the waste, and the waste is the reason the response body belongs in the log.

This is the same misdirection as [the TCP retransmission that showed up as the p99](https://debugly.dev/tcp-retransmit-becomes-p99/), and the common root is the property, not the platform.

## What actually fixed it

**Used multipart upload with a fresh signature per part.** The parts are the small, and the small is the fast, and the fast is the inside the window, and the inside is the fix, and the fix is the multipart, and the multipart is the resume, and the resume is the retry's cheap, and the cheap is the customer's bandwidth respect.

**Derived the window from the file size and the expected bandwidth.** The window is the estimate plus the margin, and the margin is the slow connection, and the slow is the covered, and the covered is the fix, and the fix is the calculation, and the calculation is the discipline, because the fixed window is the guess and the guess fails on the large file.

**Logged the response body on the failure.** The body is the reason, and the reason is the expired, and the expired is the diagnosis, and the diagnosis is the fix, and the fix is the log, and the log is the one line, and the line is the hour's saving, because the status code alone is the ambiguous.

**Handled the expiry as a distinct error and re-signed.** The 403 with the expired body is the re-sign, and the re-sign is the resume, and the resume is the fix, and the fix is the branch, and the branch is the discipline, because the generic retry restarts the upload and the restart is the wasted bandwidth.

**Checked the client clock skew.** The skew is the early expiry, and the early is the wrong clock, and the wrong clock is the client's, and the client's is the unfixable, and the unfixable is the tolerance, and the tolerance is the server's grace, and the grace is the fix, and the fix is the configuration.

## The takeaway

A presigned URL is valid for a fixed window while the upload duration depends on the file size and the customer's bandwidth, so a large upload on a slow connection outlives its signature. Use multipart with a per-part signature, derive the window from the expected transfer, log the response body, treat expiry as a distinct error that re-signs and resumes, and allow for clock skew.

The upload reached ninety percent and then failed with a 403, and the team spent an hour auditing credentials that were perfectly valid, because the real message was in the response body nobody logged and the body said the signature had expired fifteen minutes into a twenty-minute upload. Name that one property in review and this class of bug gets hard to ship.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)