skip to content

Code running in AWS Lambda generates an S3 presigned GET URL with a 24-hour expiry, but recipients start getting 403 responses a few hours later. Why does the URL die early, and what is the real ceiling on a presigned URL's lifetime?

level: middleimportance: should knowfreq 55%

answer

  1. two clocks, the shorter one wins
  2. temporary credentials carry a session token
  3. the URL cannot outlive its session
  4. SigV4 caps the requested expiry
  5. re-issue short URLs instead of extending them

basics

~20 s

Two clocks govern a presigned URL, and the shorter wins. Lambda signs with the execution role's temporary STS credentials, so the URL dies when that session expires, no matter what X-Amz-Expires says. SigV4 itself caps the requested expiry at seven days.

solid answer

~50 s

A presigned URL cannot outlive the credentials that signed it. In Lambda the SDK signs with the execution role's temporary STS credentials, which are short-lived and rotate; once that session expires, every URL signed with it fails with a 403 whose body reports an expired token rather than a bad signature. The `X-Amz-Expires` value you passed is only the *second* clock — S3 enforces whichever expires first. As of 2026, SigV4 also caps `X-Amz-Expires` at 604800 seconds, seven days, and you only get anywhere near that by signing with a long-lived IAM user access key, which is exactly the credential you should not be creating. The practical answer is to stop trying to mint long-lived URLs: issue them on demand from an authenticated endpoint with an expiry measured in minutes, and let the client come back for a fresh one.

go deeper

for a junior

Know that a presigned URL has an expiry you choose, and that it can stop working sooner if the credentials that signed it were temporary.

for a middle

Explain the two clocks — the requested expiry and the STS session behind the signature — and that S3 enforces whichever comes first, plus the seven-day SigV4 ceiling.

for a senior

Diagnose it from the error body and the cohort-shaped failure pattern, and redesign toward a durable link into your own service that re-authorizes and mints a short URL on each visit.

for a principal

Frame it as a credential-lifetime policy: pushing expiries out to reach a product requirement quietly reintroduces static keys, and the organization should standardize on short signing plus re-issue instead.

## Two clocks, and the shorter one wins When S3 receives a presigned request it checks two independent things: 1. **The URL's own expiry.** `X-Amz-Date` plus `X-Amz-Expires` gives an absolute deadline; past it, the request is rejected. 2. **The validity of the credentials named in `X-Amz-Credential`.** If those are temporary credentials from AWS STS, the URL also carries an `X-Amz-Security-Token` query parameter, and S3 validates that session token like any other. When the session ends, the token is no longer accepted. Asking for `ExpiresIn=86400` from Lambda does not extend the session; it just makes the first clock longer than the second, so the second one decides. Anywhere the SDK picks up role credentials — Lambda, an ECS task role, an EC2 instance profile, a locally assumed role — the same limit applies. ## Why the failure looks confusing The request fails with `403 Forbidden`, which people immediately read as a permissions or signature bug and start re-checking the bucket policy. The XML body is the tell: an expired session token reports an expired-token condition, whereas a mangled URL reports `SignatureDoesNotMatch` and a genuinely past deadline reports that the request has expired. Read the error code before touching policy. The other clue is the shape of the outage — URLs work perfectly for a while and then a whole cohort issued around the same time stops working together. ## The seven-day ceiling As of 2026, SigV4 refuses a presigned expiry greater than 604800 seconds (seven days); the SDK errors at signing time rather than producing a URL that would never work. And that ceiling is only reachable with a long-lived IAM user access key, because no STS session lasts a week: an assumed role session tops out at the role's configured maximum, and service-supplied credentials are shorter still and rotated for you. So "give me a link that works for a month" has no presigned-URL answer at all. Candidates who try to engineer one usually reach for an IAM user with static keys stored in the app, which trades a short-lived, scoped, automatically-rotated credential for a permanent secret that now must be guarded, rotated and audited forever — and whose leak compromises every object the signer can touch, not one. ## What to do instead The durable pattern separates *your* authorization from *S3's*: - Keep a durable, opaque link that points at **your** service, not at S3. - When it is opened, authenticate and authorize the user in your own application, decide the object key server-side, mint a presigned URL valid for a few minutes, and redirect to it or return it as JSON. - The client can come back as often as it likes; each visit re-checks your rules, so revoking a user's access actually works. This also fixes the revocation problem: a leaked five-minute URL is nearly harmless, while a leaked seven-day URL is an incident. ```python # Sign short and re-issue; do not try to outlive the session. url = s3.generate_presigned_url( "get_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=300, ) ``` ## Two related traps **Clock skew.** The deadline is computed from `X-Amz-Date`, which comes from the signer's clock. A signing host whose clock is badly wrong produces URLs that are already expired, or that S3 rejects as being from the future. On a container host with drifting time this shows up as intermittent 403s on freshly minted URLs. **Caching and sharing.** Because the signature is in the query string, a presigned URL is part of the cache key for anything in front of it and part of the address a user can copy out of the address bar. Long expiries multiply both effects: the URL ends up cached in a CDN, pasted into a ticket, and indexed in someone's browser history, all still valid. ## The summary an interviewer wants "The URL inherits the lifetime of the credentials that signed it. Under a role, that is the session — often much shorter than the expiry I asked for. SigV4 caps the requested expiry at seven days regardless, and reaching that requires static keys. So I sign short and re-issue from an authenticated endpoint instead of trying to hand out long-lived links."

  • How would you tell an expired-session failure apart from a corrupted URL, given both return 403?
    Read the XML error body rather than the status line. A tampered or truncated URL reports `SignatureDoesNotMatch`; a URL whose deadline has passed reports that the request has expired; a dead STS session reports an expired token. The timing helps too — session expiry knocks out a whole cohort of URLs issued around the same moment, while signature errors hit individual requests.
  • A colleague proposes creating an IAM user with static access keys purely so presigned URLs can last seven days. What is your objection?
    It swaps an automatically rotated, short-lived credential for a permanent secret that must be stored, rotated and audited forever, and whose leak exposes everything that principal can reach. The requirement behind it — a link that stays valid — is better served by a durable link into your own service that re-authorizes on each visit and mints a fresh few-minute URL.
  • Why can a correct-looking presigned URL fail immediately after it is generated?
    Usually clock skew on the signing host. The deadline is derived from `X-Amz-Date`, so a host whose time is well ahead or behind produces URLs S3 considers expired or not yet valid. It presents as intermittent 403s on brand-new URLs from one machine; fixing time sync on that host resolves it.

saying these in an interview costs you the question

  • Assumes X-Amz-Expires alone decides when the URL dies
  • Thinks presigning from Lambda uses account root or long-term keys
  • Says presigned URLs can be made to last indefinitely
  • Reaches for a static IAM access key to get longer expiries
  • Blames the bucket policy for every 403 without reading the error code

context