Engineering research / aws

Temporary AWS Credentials in Long-Running Jobs: Session Duration, the Role-Chaining Hour, Presigned URLs That Expire Early and Refresh

Why a batch job, export or migration that runs for hours fails with ExpiredToken at an awkward moment, how AssumeRole session duration and the one-hour role-chaining limit work, why presigned URLs die with the credentials that signed them, and how to structure clients so credentials refresh.

By Sandeep Belgavi · Published 2026-10-26

Core engineering · Practical technical guide · 26 October 2026

Why a batch job, export or migration that runs for hours fails with ExpiredToken at an awkward moment, how AssumeRole session duration and the one-hour role-chaining limit work, why presigned URLs die with the credentials that signed them, and how to structure clients so credentials refresh.

The failure in the middle of the night

A data export starts at 01:00, runs for three hours and dies at 02:00 with ExpiredToken. A migration tool that worked in testing fails in production two hours in. A link in a report email, presigned for seven days, stops working the same afternoon. Each of these is the same mistake: temporary credentials have a lifetime, and something in the job took a snapshot of them instead of asking for fresh ones.

Temporary credentials from AWS STS are an access key, a secret key and a session token with an expiry. The SDKs refresh them automatically when they come from a provider that can be asked again: an assume-role profile, web identity, the container or instance metadata endpoints, or IAM Identity Center. They cannot refresh credentials that were handed to them as fixed values, such as keys passed into a client constructor or exported as environment variables by a wrapper script. A quick check with botocore 1.43.116 shows the difference: a Boto3 session built from explicit keys returns a plain Credentials object, which has no way to refresh itself.

How long a session can last

How the credentials were obtainedLifetime per AWS documentation
AssumeRole with DurationSeconds900 seconds up to the role maximum session duration; default 3,600 seconds
Role maximum session durationA role setting from 1 hour to 12 hours
Role chaining: assuming a role with credentials from another roleLimited to one hour; DurationSeconds above one hour makes AssumeRole fail
GetSessionToken for an IAM userDefault 12 hours, from 15 minutes up to 36 hours; one hour for the root user
Presigned URL signed with temporary credentialsExpires when the credentials expire, even if a longer expiry was requested

Role chaining catches many teams. A CI runner that assumes a deployment role, which then assumes a role in a target account, is chaining, and the second session is capped at one hour whatever the target role's maximum says. The IAM documentation applies the limit to console role switching, the CLI and the API alike.

Presigned URLs inherit the expiry of their signer

A presigned URL is not a separate credential: it is a request signed with your credentials, and S3 validates it against them when it is used. The S3 documentation is explicit that a URL created with temporary credentials expires when the credentials expire, even if you specified a longer expiration, and that a URL can be valid for up to seven days only when signed with long-term IAM user credentials. Its troubleshooting notes add typical lifetimes: ECS task role credentials rotate every one to six hours, instance profile credentials have a maximum validity of about six hours, and an AssumeRole session lasts one hour by default.

So a report job running on ECS that presigns a seven-day link produces a link that may die within the hour. If links must outlive the job, sign them with a dedicated identity whose credentials last at least as long as the link, or serve downloads through an endpoint that checks your own authorization and presigns on demand.

A worked example: a three-hour export

An export job on an EC2 instance assumes a role in the data account with a 12-hour maximum session, using credentials it got from the instance profile, then writes Parquet files to S3 for three hours. The first version reads the instance credentials, calls AssumeRole once, and builds the S3 client with the returned keys. It fails at about 60 minutes, for two reasons: the AssumeRole call defaulted to 3,600 seconds, and because the source credentials came from a role, this is role chaining, so asking for longer would have failed anyway.

Illustrative Python; prefer a profile with role_arn and credential_source in ~/.aws/config, which gives the same refresh without private attributes.

import boto3
from botocore.credentials import DeferredRefreshableCredentials
from botocore.session import get_session

ROLE = "arn:aws:iam::111122223333:role/export-writer"

def assume():
    creds = boto3.client("sts").assume_role(
        RoleArn=ROLE, RoleSessionName="nightly-export",
        DurationSeconds=3600)["Credentials"]          # chained: one hour is the cap
    return {"access_key": creds["AccessKeyId"],
            "secret_key": creds["SecretAccessKey"],
            "token": creds["SessionToken"],
            "expiry_time": creds["Expiration"].isoformat()}

def refreshing_session():
    core = get_session()
    core._credentials = DeferredRefreshableCredentials(
        refresh_using=assume, method="sts-assume-role")
    return boto3.Session(botocore_session=core)

s3 = refreshing_session().client("s3")   # refreshes before expiry, for hours

The cleaner fix needs no code. A profile in ~/.aws/config with role_arn and credential_source set to Ec2InstanceMetadata makes Boto3 call AssumeRole for you; its documentation says it caches the temporary credentials in memory and refreshes them automatically when they expire. The session still lasts at most an hour, but the client fetches a new one before it ends, so the job can run for as long as it needs. The snippet shows the same mechanism when you cannot use a profile; it relies on a private attribute and should be a last resort.

Structuring long jobs

  1. Create clients from a provider that can refresh: an assume-role profile, web identity, container or instance credentials. Never construct clients from copied keys in a long job.
  2. Avoid role chaining where the job runs long; grant the needed permissions to the first role, or let the platform assume the target role directly.
  3. Set DurationSeconds deliberately and the role maximum session duration to match, knowing that refresh, not duration, is what makes long jobs safe.
  4. Make the job resumable by checkpointing progress, so a credential failure costs minutes, not the whole run.
  5. Sign presigned URLs with credentials that outlive the link, or presign at download time.

Failure modes

  • Wrapper scripts that export keys. A shell step that runs aws sts assume-role and exports the three variables gives every later process fixed credentials with a one-hour clock.
  • Clients created once at start-up in a worker pool. If they were built from fixed keys, every worker fails at the same moment.
  • Multipart uploads that span the expiry. Parts signed after expiry fail; with a refreshing client, later parts are signed with new credentials.
  • Clock skew. A host clock far off makes valid credentials look expired or not yet valid; keep time synchronised.

Verification

In a test account, set the role maximum session duration to one hour, run the job for longer than an hour with a refreshing client, and confirm in CloudTrail that AssumeRole was called again before the first session expired. Presign a URL from a role session with a seven-day expiry and confirm that it fails after the session ends. If your jobs all finish well within an hour, or run on a platform whose SDK credentials refresh by default and you never copy keys, none of this needs changing, but the presigned-URL rule still applies.

Sources and scope

Checked against these sources on 26 October 2026. Evergreen guide based on AWS and Boto3 documentation read on this date; the export example is illustrative.

Build long-running clients from credential providers that refresh, avoid role chaining where jobs run for hours because it caps sessions at one hour, never pass copied keys into long jobs, and sign presigned URLs with credentials that outlive the link.