How to upload Selenium screenshots to Amazon S3
Save Selenium screenshots as PNG files and upload them to Amazon S3 with Boto3, or use a presigned URL when your test runner should not hold AWS credentials.
The simplest way to upload a Selenium screenshot to Amazon S3 is to save the current browser window to a PNG file with Selenium, then upload that file to a bucket and object key with Boto3. Give each capture a unique key so parallel tests do not overwrite one another. If the test runner should not hold AWS credentials, have a trusted service create a short-lived presigned upload URL instead.
1. Save a Selenium screenshot and upload it with Boto3
This Python example captures the current browser window, creates the local artifact directory, checks that Selenium saved the image, and uploads it to S3. It assumes driver is an initialized Selenium WebDriver. Replace the bucket, key, and driver setup with values for your test suite.
from pathlib import Path
import boto3
bucket = "your-bucket"
key = "selenium/run-123/test-checkout.png"
local_path = Path("artifacts/test-checkout.png")
local_path.parent.mkdir(parents=True, exist_ok=True)
# driver must be an initialized Selenium WebDriver.
if not driver.save_screenshot(str(local_path)):
raise RuntimeError("Selenium did not save the screenshot")
s3 = boto3.client("s3")
s3.upload_file(str(local_path), bucket, key)
print(f"Uploaded s3://{bucket}/{key}")
save_screenshot(filename) writes the current window screenshot as a PNG and returns whether it saved successfully. Boto3’s upload_file(filename, bucket, object_name) transfers a local file to the selected bucket and key. The transfer helper supports multipart transfers for large files; ordinary screenshots may not need that behavior. [Selenium WebDriver API; Boto3 S3 file upload guide]
Install dependencies and configure AWS access
python -m pip install selenium boto3
Configure AWS credentials and the target Region through an AWS-supported runtime credential mechanism, such as the environment or role assigned to the CI runner. The identity needs permission to upload to the intended bucket and key. Do not put long-lived credentials in source code or commit them to the repository. Boto3’s client uses its normal credential and Region configuration chain.
For a standalone script, initialize and close the driver around the capture:
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--headless")
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com")
# Run the capture and upload code here.
finally:
driver.quit()
In a test framework, capture before the driver is quit and after the page reaches the state you want to inspect. The example is based on the separately documented Selenium and Boto3 interfaces; it is not a claim that this combined integration has been tested.
Choose a collision-resistant object key
An S3 object is addressed by bucket and key. Use a key that helps identify the run and test, for example selenium/<run-id>/<test-name>/<timestamp>.png. If two runs upload to the same key, the later upload replaces the object at that key unless bucket versioning is configured. Do not assume versioning is enabled.
2. Upload a file-like object instead of a path
If the screenshot is already stored in a binary stream, Boto3 also provides upload_fileobj. Open the file in binary mode so the stream yields bytes:
from pathlib import Path
import boto3
local_path = Path("artifacts/test-checkout.png")
bucket = "your-bucket"
key = "selenium/run-123/test-checkout.png"
with local_path.open("rb") as image_file:
boto3.client("s3").upload_fileobj(image_file, bucket, key)
Use upload_file when you have a filesystem path; use upload_fileobj when a readable binary file object is more convenient. Both target a bucket and object key. [Boto3 upload methods]
3. Use a presigned URL when the runner should not hold AWS credentials
A trusted backend can create a presigned URL for a specific object and operation, then pass it to the test runner. The runner uploads the PNG with that URL without receiving general AWS credentials. The URL grants only what its creator is permitted to grant, and possession of the URL is enough to use it until it expires or the signing credentials become invalid. AWS describes presigned URLs as temporary access for users without AWS credentials. [AWS: Uploading objects with presigned URLs; Boto3 presigned URLs]
Generate a presigned PUT URL in a trusted service
import boto3
s3 = boto3.client("s3", region_name="us-east-1")
url = s3.generate_presigned_url(
ClientMethod="put_object",
Params={
"Bucket": "your-bucket",
"Key": "selenium/run-123/test-checkout.png",
"ContentType": "image/png",
},
ExpiresIn=900,
)
print(url)
Return the URL to the runner over an authenticated channel. The example signs the content type, so the upload must send the same value.
Upload with the presigned URL from the test runner
from pathlib import Path
import requests
url = "PRESIGNED_PUT_URL_FROM_TRUSTED_SERVICE"
image_path = Path("artifacts/test-checkout.png")
with image_path.open("rb") as image_file:
response = requests.put(
url,
data=image_file,
headers={"Content-Type": "image/png"},
timeout=90,
)
response.raise_for_status()
For a presigned POST instead, the service returns a URL and a fields mapping; submit the returned fields and file as a multipart form exactly as provided. Do not treat a POST form and a PUT URL as interchangeable. [Boto3 presigned URL guide]
Protect URL lifetime and object replacement
- Keep presigned URLs out of source control, public logs, and user-visible output. Treat them like bearer tokens.
- Set an expiry appropriate to the workflow. The effective lifetime can be shorter than the requested expiry when the credentials used to sign the URL are temporary, revoked, or deactivated.
- Use a unique object key for each capture. Uploading to an existing key replaces that object.
- For SigV4 requests with a checksum, send the matching checksum header and algorithm expected by the signed request.
AWS documents a maximum presigned URL lifetime of up to seven days for eligible SDK or CLI credentials; temporary role credentials can expire earlier. [AWS presigned URL guidance]
4. Run the capture in CI
- Start the browser and navigate to the page under test.
- Wait for the page state needed for the screenshot, then save the PNG before quitting the browser.
- Upload to a key containing the run identifier and test name, using the runner’s AWS role or a presigned URL.
- Retain the local artifact as a CI artifact too if it helps diagnose failures without opening S3.
- Make upload failure visible. Decide whether an S3 outage should fail the test, mark artifact publishing as failed, or trigger a retry according to your pipeline’s requirements.
The last two choices are workflow design recommendations: the right behavior depends on whether screenshot storage is required for test correctness or only for debugging. Avoid silently swallowing upload exceptions, since that can make missing artifacts hard to diagnose.
5. Choose the credential pattern
| Pattern | Use when | What the runner needs |
|---|---|---|
| Direct Boto3 upload | The runner has an approved AWS identity and needs to upload to the bucket. | A role or credential configuration with permission for the target bucket and key. |
| Presigned PUT or POST | A trusted component can authorize a specific upload, and the runner should not receive AWS credentials. | The URL, plus the exact method, fields, and signed headers required by it. |
Direct upload is the shorter path when the runner already operates with a suitable AWS identity. Presigned upload narrows what the runner can do and for how long, but requires a trusted URL-issuing component and careful handling of the URL.
6. Performance, reliability, and cost considerations
Performance
The workflow has two steps: browser capture to local storage, then transfer to S3. Boto3’s transfer helper can split large files into chunks and upload them in parallel. This is a capability of the SDK, not a benchmark for screenshot uploads. For ordinary screenshot artifacts, browser startup and page loading may dominate total time; measure your own CI pipeline before tuning transfer settings.
Reliability
- Check the boolean returned by
save_screenshotand allow upload exceptions to reach the pipeline’s error handling. - Use unique keys so concurrent tests do not overwrite each other’s evidence.
- Use timeouts in the runner’s HTTP request for presigned uploads and report non-success status codes.
- For retries, reuse the intended key only if replacing that object’s content is acceptable; otherwise create a new attempt-specific key.
- Keep a local or CI artifact copy if S3 availability is not required for the test itself.
Cost
S3 charges depend on the AWS account’s storage, request, transfer, and retention details. This workflow’s research does not establish a fixed per-screenshot price. Check current AWS pricing for the bucket’s Region and your storage lifecycle, request volume, and data transfer pattern before estimating cost.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
save_screenshot returns false or no file appears |
The driver is no longer active, the parent directory is missing, or the capture failed. | Capture before driver.quit(), create the directory first, and check the method’s return value. |
NoCredentialsError or access denied |
The runner has no usable AWS identity, or its permissions do not allow the requested upload. | Configure the runner’s AWS role or supported credential source and grant only the needed write access to the target location. |
SignatureDoesNotMatch for a presigned upload |
The URL may have been altered or expired; the system clock, Region, HTTP method, content type, or signed headers may not match. | Use the URL unchanged, confirm the URL is still valid, synchronize the clock, verify the bucket Region and method, and send exactly the signed headers. |
| Presigned URL returns an expired or invalid request error | The configured expiry elapsed, or temporary signing credentials expired or were revoked. | Request a fresh URL and generate it close to upload time with a suitable expiry. |
| Uploaded image is missing or another test’s image appears | Tests reused the same object key, so one upload replaced another. | Include a run ID, test name, and unique capture identifier in each key. |
| Presigned POST fails although PUT works | The client used the wrong upload protocol or omitted form fields. | For POST, submit the exact returned URL and fields as multipart form data; for PUT, send the file body to the URL with required signed headers. |
| Upload succeeds but the image metadata or content type is unexpected | The object request did not set the intended content type, or a presigned request’s signed content type did not match. | Set ContentType="image/png" in the signed object parameters and send the same Content-Type header during upload. |
AWS’s presigned upload troubleshooting specifically calls out altered or expired URLs, clock synchronization, content type, and bucket Region as checks for SignatureDoesNotMatch. [AWS presigned upload troubleshooting]
8. Or skip the browser setup
If you need a hosted screenshot without managing Selenium and a browser runner, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. See the API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed, and known consent banners, newsletter popups, and chat widgets are removed before the shot; each step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing. Response headers report the page verdict and whether the request was billed.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents. - The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
9. FAQ
Does Selenium save a full-page screenshot?
save_screenshot captures the current browser window. If you need a full-page capture, use a browser-specific approach or a screenshot service that supports full-page capture.
Can I upload without saving a screenshot to disk?
Use a readable binary file object with Boto3’s upload_fileobj. Selenium’s documented save_screenshot interface writes to a filename, so the straightforward workflow first creates a file.
Will uploading to the same key keep both screenshots?
No. Uploading to an existing key replaces its current object unless the bucket’s versioning configuration preserves prior versions.
Does a presigned URL give the runner general AWS access?
No. It authorizes the operation represented by that URL, subject to the creator’s permissions and the URL’s effective validity period. Protect it like a secret.


