# Why Your boto3 S3 Client Returns Empty Results: 10 Common Causes and Fixes

> Encountering empty results with your boto3 S3 client Investigate common causes like bucket config, region issues, IAM permissions, and pagination. Fix your S3 client today.

- Repository: [Python Software Foundation/requests](https://github.com/psf/requests)
- Tags: how-to-guide
- Published: 2026-02-20

---

**TLDR:** When the boto3 S3 client returns unexpected empty results, the cause is almost always incorrect bucket configuration, region mismatches, missing IAM permissions, or unhandled pagination—not a bug in the library itself.

The boto3 library relies on HTTP communication handled by underlying transport layers, including the **requests** library as implemented in `psf/requests`. When `list_objects_v2` or similar API calls return empty `Contents` arrays without raising exceptions, the issue typically stems from how the request is constructed or how the AWS environment is configured. Understanding both the S3 API behavior and the HTTP transport layer—where [`src/requests/sessions.py`](https://github.com/psf/requests/blob/main/src/requests/sessions.py) manages connection pooling and [`src/requests/adapters.py`](https://github.com/psf/requests/blob/main/src/requests/adapters.py) handles the actual HTTPAdapter implementation—helps diagnose these silent failures.

## Configuration Errors That Cause Empty Results

### Incorrect Bucket Names and Typos

S3 does not validate bucket existence during `ListObjectsV2` calls in the way you might expect. If you provide a bucket name that does not exist—or contains a typo—S3 silently returns an empty list rather than raising a `NoSuchBucket` error. This behavior occurs because the API interprets the request as a valid query against a resource you may not have permission to know exists.

Always verify bucket names using the AWS CLI (`aws s3 ls`) or console before debugging code. In [`src/requests/models.py`](https://github.com/psf/requests/blob/main/src/requests/models.py), the `PreparedRequest` object constructs the final URL, so a malformed bucket name in the endpoint URL will still result in a valid HTTP 200 response from S3’s API, masking the configuration error.

### Region Mismatches in Client Setup

If your boto3 client is configured for a different region than where the bucket actually resides, S3 returns a 301 redirect. While boto3 follows redirects automatically, the subsequent request may still be scoped incorrectly, or the redirect handling may strip certain query parameters, resulting in an empty object list.

Explicitly set the `region_name` parameter when creating the client:

```python
s3 = boto3.client('s3', region_name='us-west-2')

```

The HTTP layer—handled by [`src/requests/adapters.py`](https://github.com/psf/requests/blob/main/src/requests/adapters.py) in the underlying requests library—manages the connection pool and SSL verification across these regional endpoints, but it cannot correct a fundamental region mismatch in the AWS signature.

## Permission and Policy Constraints

### Missing IAM Permissions

A `ListObjectsV2` call without the `s3:ListBucket` permission does not raise an `AccessDenied` error in all contexts. Instead, S3 returns a response with an empty `Contents` array. This silent failure makes permission debugging particularly challenging.

Verify your IAM policy includes:

```json
{
    "Effect": "Allow",
    "Action": [
        "s3:ListBucket",
        "s3:GetObject"
    ],
    "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
    ]
}

```

### VPC Endpoint Policy Denials

When accessing S3 through a VPC endpoint, the endpoint policy acts as an additional authorization layer. If the endpoint policy denies `ListBucket` actions, the request succeeds at the network level—returning HTTP 200—but returns an empty result set.

Review your VPC endpoint policy in the AWS console to ensure it permits the necessary S3 actions for your IAM principal.

## Request Parameter Problems

### Incorrect Prefix or Delimiter Usage

Supplying a `Prefix` parameter that does not match any object keys filters the result set to zero items. Common mistakes include:
- Including a leading slash when objects are stored without one
- Omitting a trailing slash when searching for "directory" prefixes
- Case sensitivity mismatches

List the bucket without filters first to establish a baseline, then refine with prefixes.

### Unhandled Pagination

`list_objects_v2` returns a maximum of 1,000 keys per request. If your bucket contains more objects and you only check the first response page, you may incorrectly conclude the bucket is empty or missing data.

Use the boto3 paginator to handle this automatically:

```python
paginator = s3.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket='my-bucket'):
    for obj in page.get('Contents', []):
        print(obj['Key'])

```

## Consistency and Object State Issues

### S3 Eventual Consistency

After a `PUT` or `DELETE` operation, S3 provides read-after-write consistency for new objects in most regions, but list operations may lag. If you upload an object and immediately list the bucket, the new object may not appear.

Implement a retry loop with exponential backoff:

```python
import time

def list_with_retry(bucket, max_retries=5):
    for i in range(max_retries):
        resp = s3.list_objects_v2(Bucket=bucket)
        if resp.get('KeyCount', 0) > 0:
            return resp['Contents']
        time.sleep(2 ** i)
    return []

```

### Object Lock and Legal Hold Restrictions

Objects under a legal hold or governed by Object Lock retention policies can be listed but may be inaccessible for reads depending on your permissions. This can create confusion when downstream code expects to retrieve data from listed objects.

Check the bucket's Object Lock configuration in the S3 console if you encounter `AccessDenied` errors on objects that appear in listings.

## Debugging Steps for boto3 S3 Client Issues

When troubleshooting empty results, follow this systematic approach:

1. **Verify credentials** – Run `aws sts get-caller-identity` to confirm your identity and permissions.
2. **Check bucket existence** – Use `aws s3 ls s3://my-bucket` to confirm the bucket is accessible.
3. **List without filters** – Call `list_objects_v2` with only the `Bucket` parameter to establish a baseline.
4. **Inspect the response** – Examine `KeyCount`, `IsTruncated`, and `Contents` fields to distinguish between empty buckets and truncated results.
5. **Enable HTTP logging** – Set `boto3.set_stream_logger('botocore')` to view raw HTTP requests and responses, revealing redirects or permission issues at the transport layer.

## Practical Code Examples

### Listing Objects with Error Handling and Pagination

This example demonstrates robust listing with proper pagination handling using the boto3 paginator:

```python
import boto3
from botocore.exceptions import ClientError

s3 = boto3.client('s3', region_name='us-west-2')

def list_all_objects(bucket, prefix=''):
    """
    List all objects in a bucket with automatic pagination.
    Handles permissions errors and empty results gracefully.
    """
    paginator = s3.get_paginator('list_objects_v2')
    
    try:
        for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
            contents = page.get('Contents', [])
            for obj in contents:
                print(f"Found: {obj['Key']}")
            
            if not contents:
                print(f"No objects found in page (KeyCount: {page.get('KeyCount', 0)})")
                
    except ClientError as e:
        error_code = e.response['Error']['Code']
        print(f"AWS Error {error_code}: {e.response['Error']['Message']}")

list_all_objects('my-bucket', prefix='logs/')

```

### Verifying IAM Permissions Before Listing

Use IAM policy simulation to verify permissions before making expensive API calls:

```python
import boto3
import json

iam = boto3.client('iam')
sts = boto3.client('sts')

def can_list_bucket(bucket_name):
    """
    Simulate the principal's policy to verify s3:ListBucket permission.
    Returns True if allowed, False otherwise.
    """
    bucket_arn = f'arn:aws:s3:::{bucket_name}'
    caller_arn = sts.get_caller_identity()['Arn']
    
    try:
        response = iam.simulate_principal_policy(
            PolicySourceArn=caller_arn,
            ActionNames=['s3:ListBucket'],
            ResourceArns=[bucket_arn]
        )
        
        results = response['EvaluationResults']
        return all(
            result['EvalDecision'] == 'allowed' 
            for result in results
        )
        
    except Exception as e:
        print(f"Permission check failed: {e}")
        return False

if can_list_bucket('my-bucket'):
    print("Permission granted, proceeding with listing...")
else:
    print("Missing s3:ListBucket permission")

```

### Handling Eventual Consistency with Retry Logic

Implement exponential backoff to handle S3's eventual consistency after recent writes:

```python
import time
import boto3
from botocore.exceptions import ClientError

s3 = boto3.client('s3')

def list_with_consistency_retry(bucket, max_tries=5, base_delay=2):
    """
    List objects with retry logic to handle S3 eventual consistency.
    Useful immediately after PUT/DELETE operations.
    """
    for attempt in range(max_tries):
        try:
            response = s3.list_objects_v2(Bucket=bucket)
            key_count = response.get('KeyCount', 0)
            
            if key_count > 0 or attempt == max_tries - 1:
                return response.get('Contents', [])
            
            # Exponential backoff: 2s, 4s, 8s...

            sleep_time = base_delay * (2 ** attempt)
            print(f"No objects yet, retrying in {sleep_time}s...")
            time.sleep(sleep_time)
            
        except ClientError as e:
            print(f"API Error: {e.response['Error']['Message']}")
            raise
    
    return []

objects = list_with_consistency_retry('my-bucket')
print(f"Retrieved {len(objects)} objects")

```

## The Role of HTTP Transport in boto3 Operations

While boto3 provides the high-level AWS API abstraction, it relies on underlying HTTP libraries to execute requests. The **requests** library (`psf/requests`) often serves as the transport layer for botocore (boto3's underlying library), handling connection pooling, SSL verification, and redirect following.

Key files in the requests repository that support boto3's HTTP operations include:

- **[`src/requests/sessions.py`](https://github.com/psf/requests/blob/main/src/requests/sessions.py)** – Manages session state and connection pooling via `Session` objects, which boto3 uses to maintain persistent connections to S3 endpoints.
- **[`src/requests/adapters.py`](https://github.com/psf/requests/blob/main/src/requests/adapters.py)** – Implements the `HTTPAdapter` class that handles the actual HTTP transmission, including handling the 301 redirects that occur during region mismatches.
- **[`src/requests/models.py`](https://github.com/psf/requests/blob/main/src/requests/models.py)** – Defines `PreparedRequest` and `Response` objects; when S3 returns an empty XML body with HTTP 200, this is the layer that first processes that response.
- **[`src/requests/exceptions.py`](https://github.com/psf/requests/blob/main/src/requests/exceptions.py)** – Contains the exception hierarchy; while S3 permission issues often return empty lists rather than HTTP errors, connection failures raise `ConnectionError` exceptions defined here.

When debugging empty S3 results, enabling botocore logging (`boto3.set_stream_logger('botocore')`) reveals the raw HTTP requests and responses processed by these underlying components, helping distinguish between transport-level issues and S3 API behavior.

## Summary

- **Configuration errors** such as incorrect bucket names or region mismatches cause S3 to return empty lists rather than errors, requiring explicit verification of `region_name` and bucket spelling.
- **IAM permissions** and **VPC endpoint policies** can silently filter results when `s3:ListBucket` is denied, returning HTTP 200 with empty `Contents` arrays instead of `AccessDenied` errors.
- **Request parameters** including incorrect `Prefix` values or failure to handle pagination via `KeyCount` and `IsTruncated` fields commonly lead to perceived empty buckets.
- **Eventual consistency** after write operations requires retry logic with exponential backoff, particularly in the `us-east-1` region.
- **Underlying HTTP transport** handled by the **requests** library (`psf/requests`) in files like [`src/requests/sessions.py`](https://github.com/psf/requests/blob/main/src/requests/sessions.py) and [`src/requests/adapters.py`](https://github.com/psf/requests/blob/main/src/requests/adapters.py) processes the HTTP 200 responses that contain empty S3 XML bodies.

## Frequently Asked Questions

### Why does `list_objects_v2` return an empty `Contents` list instead of an access denied error?

S3 returns HTTP 200 with an empty `Contents` array when your IAM user or role lacks `s3:ListBucket` permission, rather than raising a 403 error. This design prevents bucket enumeration attacks. Verify permissions using `iam.simulate_principal_policy()` or check the IAM policy attached to your credentials for the `s3:ListBucket` action on the specific bucket ARN.

### How do I verify if a bucket exists before attempting to list objects?

Use `head_bucket` instead of `list_objects_v2` to verify bucket existence and accessibility. This method returns a 200 response only if the bucket exists and you have permission to view it, raising `ClientError` with `NoSuchBucket` or 403 otherwise. Alternatively, use `aws s3 ls s3://bucket-name` from the CLI to quickly verify accessibility without writing code.

### Can VPC endpoint policies cause empty S3 results without network errors?

Yes. When accessing S3 through a VPC endpoint, the endpoint policy acts as an additional authorization layer. If the endpoint policy denies `s3:ListBucket` while your IAM policy allows it, the request reaches S3 successfully but returns an empty list. Check the VPC endpoint policy in the AWS console to ensure it permits the necessary S3 actions for your IAM principal's ARN.

### Why do I see objects in the AWS console but get empty results from boto3?

The AWS console uses different credentials and often different regions than your boto3 client. Common causes include: the console defaulting to a different region than your code, the console using root or admin credentials while your code uses limited IAM roles, or the console showing cached results while your code hits an eventually consistent API endpoint. Verify your client's `region_name` and credentials match the console context exactly.