Why does my S3 presigned URL expire before its expiry time?
The short answer
A presigned URL works until the earlier of two moments:
1. The expiry you asked for (X-Amz-Expires, counted from X-Amz-Date).
2. The moment the credentials that signed it expire or are revoked.
Amazon's presigned URL documentation says this directly. If you sign with temporary credentials, the URL expires when the token expires, even if you asked for a later expiry. That explains most cases of "I set 7 days and it died after a few hours."
Step 1: Read the expiry from the URL
Everything you need is in the query string:
`
...?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA.../20261016/us-east-1/s3/aws4_request
&X-Amz-Date=20261016T100000Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=...
`
Nominal expiry = X-Amz-Date + X-Amz-Expires seconds. In this example that's 10:00 UTC + 1 hour = 11:00 UTC.
Check these two things:
- Access key prefix in
X-Amz-Credential. Keys starting withAKIAare long-term IAM user keys. Keys starting withASIAare temporary. X-Amz-Security-Token. If it's present, the URL was signed with temporary credentials and can't outlive them.
Step 2: Match the error to the cause
AccessDeniedwith "Request has expired": the requested expiry has passed. The XML includesExpiresandServerTime, so you can see by how much.ExpiredToken: the temporary credentials that signed the URL have expired, even thoughX-Amz-Expireshasn't run out.AuthorizationQueryParametersErrorwhen you create or use the URL: often anX-Amz-Expiresabove the limit (see below).- Expired right away: the clock on the machine that generated the URL was wrong. See the RequestTimeTooSkewed guide.
The limits
- SigV4 maximum: 7 days, which is
604800seconds. Asking for more fails. - S3 console: AWS's documentation says the console limits presigned URLs to between 1 minute and 12 hours.
- Temporary credentials: the URL lasts no longer than the session. This covers credentials from IAM roles, IAM Identity Center/SSO,
AssumeRole, EC2 instance profiles, ECS tasks and Lambda execution roles. Sessions are typically hours, not days.
Step 3: Choose a fix
Generate URLs on demand. This is the usual answer. Don't email a 7-day link. Link to your own endpoint, and have it create a fresh short-lived presigned URL and redirect each time someone clicks. Expiry stops mattering, and you keep control.
Sign with long-term credentials, if you really need a long-lived URL. Use a dedicated IAM user whose policy allows only s3:GetObject on the right prefix. This is a security trade-off: the key has to live somewhere your app can read it, and anyone who has the URL can use it until it expires.
Use a longer role session. If you assume a role, you can request a longer DurationSeconds, up to the role's configured maximum session duration. The URL still can't exceed that session.
Get the units right. Most SDKs take expiry in seconds. Writing ExpiresIn: 7 gives you a seven-second URL.
Pitfalls
- Rotation looks random. A server using instance or task role credentials gets new credentials on a schedule. URLs signed just before a rotation die sooner than URLs signed just after.
- Revocation counts too. Deleting or deactivating the access key, or revoking role sessions, kills every URL it signed.
- Downloads in progress. AWS's docs say S3 checks expiry when the request starts, so a download already running can finish. A resume or retry after expiry fails.
- Caching. A CDN or browser that caches a page with the URL in it can hand out an expired link. Generate URLs as late as possible.
Inspecting a URL in one step
You can do all of the above by splitting the query string by hand. We make Presign Pad, a free browser tool that does it in one step. Paste any presigned URL to see its credential scope, signing date, expiry, time left, signed headers, and warnings such as a temporary-credential token. Add the secret if you also want to check that the signature matches.