Low-Level Clients vs the Resources API
boto3 gives you two ways to talk to many services: the low-level client and the higher-level Resources API. This page explains why new code should default to clients, and where resources still earn their place.
Search across all documentation pages
boto3 gives you two ways to talk to many services: the low-level client and the higher-level Resources API. This page explains why new code should default to clients, and where resources still earn their place.
The Resources API is a boto3-only concept, so the boto3 examples are plain Python. Where it helps, we contrast with the AWS SDK for JavaScript v3 in prose.
Default to the low-level client. Its methods map one-to-one onto the AWS API, so everything in the API reference is available and nothing is hidden.
import boto3
# Low-level client: explicit, complete, and the actively developed surface.
s3 = boto3.client("s3")
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="my-bucket", Prefix="logs/"):
for obj in page.get("Contents", []):
print(obj["Key"], obj["Size"])When to reach for this:
The same "delete everything under a prefix" task, first with a resource (concise) and then with a client (explicit). Both are correct; the trade-off is readability versus visibility.
import boto3
# --- Resources API: reads like objects, but each step may hit the network ---
s3_resource = boto3.resource("s3")
bucket = s3_resource.Bucket("my-bucket")
bucket.objects.filter(Prefix="tmp/").delete() # iterate + batch-delete, hidden
# --- Low-level client: every request is visible and under your control ---
s3 = boto3.client("s3")
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="my-bucket", Prefix="tmp/"):
keys = [{"Key": o["Key"]} for o in page.get("Contents", [])]
if keys:
s3.delete_objects(Bucket="my-bucket", Delete={"Objects": keys})What this demonstrates:
bucket.objects are lazy; iterating them triggers API calls.get_object, put_item) maps to exactly one AWS operation, and the response is a plain dict.Bucket, Object, Table) with attributes and collections, and lazily calls the client underneath.bucket.objects.all() is simply pleasant to read..meta.client, so you are never stuck.import boto3
s3_resource = boto3.resource("s3")
client = s3_resource.meta.client # the low-level client behind the resource
client.put_object(Bucket="my-bucket", Key="k.txt", Body=b"hi")The AWS SDK for JavaScript v3 never had a resource layer. It is client-only: you construct a service client and send command objects. There is no Bucket or Table object abstraction to choose. In other words, the "clients vs resources" decision does not exist in v3, because only clients exist.
// AWS SDK v3 is client-only: no resource abstraction to weigh.
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
const out = await s3.send(new ListObjectsV2Command({ Bucket: "my-bucket", Prefix: "logs/" }));
for (const obj of out.Contents ?? []) console.log(obj.Key, obj.Size);So when you port boto3 resource code to another language, expect to rewrite it in the client style, because that is all the other SDK offers.
bucket.creation_date or iterating bucket.objects triggers API calls you did not write. Fix: know which resource attributes are lazy, or use the client when cost visibility matters.boto3.resource("newservice") raises for services without a resource layer. Fix: default to boto3.client for anything but the classic handful..meta.client deliberately when needed.| Alternative | Use When | Don't Use When |
|---|---|---|
| Low-level client | New code, full/current coverage, explicit control | You want the most concise traversal and the service has resources |
| Resources API | Quick scripts, existing resource-based code, classic services | The service has no resource layer, or you need new features or cost visibility |
Resource then .meta.client | Mostly resource style but need one missing operation | You would be simpler just using the client throughout |
| Higher-level custom wrapper | You want your own domain API over clients | A thin client call already reads clearly |
A client has one method per AWS API operation and returns dicts. A resource is an object-oriented layer over the client, with entities and collections, available for only some services.
The low-level client. The Resources API is in maintenance mode with partial coverage, so clients are the complete, actively developed surface.
It is in maintenance mode: no new features and no new services, but existing resources still work. It is not slated for removal, but it should not be your default.
Resource attributes and collections are lazy. Accessing an attribute or iterating a collection can trigger a request under the hood, which is easy to miss.
Yes. Every resource exposes its underlying client at .meta.client, so you can drop to the low-level API whenever the resource lacks something.
No. Only a subset (S3, DynamoDB, SQS, SNS, EC2, and a few more) have resources. Most newer services are client-only.
No. Create one resource per thread. If you need to share across threads, share a client instead.
No. v3 is client-only and sends command objects. There is no resource abstraction, so the choice this page describes is unique to boto3.
Not inherently, but hidden lazy calls can add round trips you did not plan for. With clients, every request is explicit, so cost is easier to reason about.
Not urgently. If it works and stays on classic services, leave it. Reach for clients when you add new features, need coverage resources lack, or want cost visibility.
Stack versions: This page was written for boto3 1.43.x on Python 3.10+ (and the AWS SDK for JavaScript v3, Node.js 18+, where contrasted).
Reviewed by Chris St. John·Last updated Jul 23, 2026