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:

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

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.

Try it