boto3's Session, Client & Resource Model
boto3 has three layered objects you need to keep straight: the Session, the client, and the resource.
Search across all documentation pages
boto3 has three layered objects you need to keep straight: the Session, the client, and the resource.
Almost every confusing boto3 behavior (wrong region, missing credentials, a NoRegionError from nowhere) traces back to which session you are really using.
This page builds the mental model so the rest of this section reads as variations on one theme. It is a Python-specific model, so the code here is plain boto3 with no cross-SDK pairing.
boto3.client("s3") at module level is not sessionless; it borrows a hidden default session that boto3 lazily creates for you.boto3.Session() whenever you juggle more than one profile, region, or thread; the default session for simple single-account scripts.Think of the three objects as layers stacked on top of the AWS API.
At the bottom is the Session. It answers one question: who am I and where am I calling? It resolves credentials from the standard chain (environment variables, shared config files, IAM roles) and pins a default region. Nothing in a session talks to AWS by itself.
In the middle is the client. A client is a thin, auto-generated wrapper whose methods map one-to-one onto AWS API operations: s3.list_buckets(), s3.put_object(...), ddb.get_item(...). This is the low-level surface, and it is what almost all modern boto3 code uses.
At the top, for some services only, is the resource. A resource is an object-oriented layer over the client, exposing things like bucket.objects.all() instead of a paginated list_objects_v2 call. It is ergonomic but legacy.
import boto3
session = boto3.Session() # 1. owns credentials + region
s3_client = session.client("s3") # 2. low-level, one method per API op
s3_resource = session.resource("s3") # 3. high-level object layer (legacy)
print(session.region_name) # e.g. "us-east-1"Every client and resource you use is spawned from some session. The only question is whether you named that session or let boto3 create a default one behind your back.
When you call boto3.client("s3") (the module-level function, not a session method), boto3 checks for a process-wide default session stored on boto3.DEFAULT_SESSION. If none exists yet, it creates one and reuses it for every later module-level call.
import boto3
# These two clients share ONE hidden default session.
s3 = boto3.client("s3")
ddb = boto3.client("dynamodb")
# The default session is created lazily on first use.
print(boto3.DEFAULT_SESSION is not None) # True after the first callThat default session is why setting AWS_PROFILE or AWS_REGION in the environment "just works" for quick scripts: the default session reads them. It is also why a script that needs two profiles cannot rely on the default session. You need explicit sessions instead.
An explicit session takes its configuration as constructor arguments:
import boto3
prod = boto3.Session(profile_name="prod", region_name="us-east-1")
dev = boto3.Session(profile_name="dev", region_name="eu-west-1")
prod_s3 = prod.client("s3") # prod credentials, us-east-1
dev_s3 = dev.client("s3") # dev credentials, eu-west-1Now each client is bound to a distinct identity and region, which is impossible with the single default session.
Credential and region resolution happen once, when the session is built and first asked for credentials, not on every call. Two clients from the same session share the same resolved credentials and the same region unless you override the region per client.
Thread safety is the sharp edge of this model, and it is worth memorizing.
Sessions are not thread-safe. A boto3.Session (including the hidden default session) should not be shared across threads. The recommended pattern is one session per thread.
Resources are not thread-safe either, so create a resource per thread as well.
Clients are thread-safe for calling operations. You can construct one client and share it across many threads safely, which is exactly why the default session plus a shared client is a fine pattern for most concurrent code.
import boto3
from concurrent.futures import ThreadPoolExecutor
# One client, shared across threads: this is safe.
s3 = boto3.client("s3")
def head(key):
return s3.head_object(Bucket="my-bucket", Key=key)["ContentLength"]
with ThreadPoolExecutor(max_workers=8) as pool:
sizes = list(pool.map(head, ["a.txt", "b.txt", "c.txt"]))The Resources API adds a second consideration: it is in maintenance mode. AWS has stated the Resources interface receives no new features, and many services (anything added in recent years) never got a resource layer at all. It still works and is not slated for removal, but new code should default to clients so you are never blocked by a missing resource.
Where the model pays off is multi-account tooling: a deployment script that assumes a role per account builds one session per account, spawns the clients it needs, and keeps each account's calls cleanly isolated.
boto3.client() has no session." - It always uses a session; if you did not pass one, it uses the lazily created default session on boto3.DEFAULT_SESSION.Credentials, a default region, and configuration (profile, retry settings, endpoint config). It is the source every client and resource draws its identity and region from.
Yes, whenever you need more than one profile, more than one credential set, or per-thread isolation. For a simple single-account script, the default session is enough.
A single boto3.Session that boto3 creates the first time you call a module-level function like boto3.client(), then reuses. It reads AWS_PROFILE, AWS_REGION, and the shared config files.
No. A client is a low-level wrapper with one method per AWS API operation. A resource is a higher-level, object-oriented layer built on top of a client, available for only a subset of services.
Clients. The Resources API is in maintenance mode, receives no new features, and does not cover newer services. Clients are the complete, actively developed surface.
Yes. Clients are thread-safe for calling operations, so constructing one and sharing it is a common and recommended pattern.
No. Sessions and resources are not thread-safe. Create one session (and one resource, if you use resources) per thread.
The session could not resolve a region from its arguments, environment, or config. Pass region_name to the session or client, or set AWS_REGION.
No. A session only resolves configuration locally. The first network call happens when a client or resource invokes an operation.
No. v3 has only per-service clients configured directly. There is no session container and no resource layer, so this split is specific to boto3.
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