How to check when an S3 presigned URL expires
Everything you need is in the URL
An S3 presigned URL (Signature Version 4) carries its signing details as query parameters. You can read them without any credentials. Here's an example, split onto lines for readability:
`
https://my-bucket.s3.eu-west-1.amazonaws.com/reports/q3.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIAEXAMPLE%2F20261016%2Feu-west-1%2Fs3%2Faws4_request
&X-Amz-Date=20261016T080000Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=5d6c...
`
Step 1: Find when it was signed
X-Amz-Date is the signing time in UTC: 20261016T080000Z means 16 October 2026, 08:00:00 UTC.
Step 2: Add the lifetime
X-Amz-Expires is the lifetime in seconds from X-Amz-Date. 3600 is one hour, so this URL stops working at 09:00:00 UTC.
`
expires_at = X-Amz-Date + X-Amz-Expires seconds
time_left = expires_at - now (UTC)
`
A quick Python check:
`python
from datetime import datetime, timedelta, timezone
from urllib.parse import urlparse, parse_qs
q = parse_qs(urlparse(url).query)
signed = datetime.strptime(q["X-Amz-Date"][0], "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc)
expires = signed + timedelta(seconds=int(q["X-Amz-Expires"][0]))
print(expires, expires - datetime.now(timezone.utc))
`
The largest value SigV4 allows for X-Amz-Expires is 604800 (7 days). Anything larger is rejected.
Step 3: Read the credential scope
X-Amz-Credential is URL-encoded. Decoded, it reads AKIAEXAMPLE/20261016/eu-west-1/s3/aws4_request:
- Access key ID: who signed it. Keys starting with
AKIAare long-term IAM user keys.ASIAmeans temporary credentials. - Date: must match the date part of
X-Amz-Date. - Region: must be the bucket's region. A mismatch here is a common reason a URL fails right away.
- Service:
s3.
Step 4: Check the signed headers
X-Amz-SignedHeaders lists headers the request must send with the exact signed values. host alone is the easiest case. If you see content-type or x-amz-* headers here, the client using the URL has to send those same headers with the same values, or S3 returns SignatureDoesNotMatch.
Why a URL can die before its expiry
The math above gives the latest possible expiry. A URL can stop working sooner:
1. Temporary credentials expired. If the URL was signed with an ASIA... key (from a role, SSO or STS), you'll also see X-Amz-Security-Token. The URL stops working when those credentials expire, even if X-Amz-Expires says longer. URLs signed from a Lambda function or an EC2 instance role often hit this, and you'll get ExpiredToken.
2. The signing key was deactivated or deleted. Every URL signed with it stops working.
3. Permissions changed. The URL only carries the signer's permissions at the time of the request. If the IAM policy or bucket policy no longer allows the action, you get AccessDenied.
4. Clock skew on the signing machine. If the server that created the URL had a wrong clock, X-Amz-Date is wrong, and the expiry moves with it.
Creating a URL with a known expiry
With the AWS CLI (GET URLs only):
`bash
aws s3 presign s3://my-bucket/reports/q3.pdf --expires-in 3600
`
For PUT uploads, use an SDK, for example generate_presigned_url in boto3 with ClientMethod='put_object'.
Pitfalls
- Treat presigned URLs as secrets until they expire. Anyone holding one can use it.
- Don't log full presigned URLs in places many people can read.
- If you shorten or re-encode the URL, check that the query string survives exactly as it was.
Inspecting a URL in one paste
We make Presign Pad, a free browser tool that does this in one step. Paste a presigned URL and it shows the credential scope, signing date, expiry, time left, signed headers and warnings. You can also add your secret to check whether the signature matches.