Published · Updated · By Elliot Jackson
What does “S3-compatible” actually mean? A checklist for local endpoints
Compare the operations, addressing, authentication, errors, and operational behaviour an application needs with what a local endpoint implements.
“S3-compatible” is one of those labels that sounds precise until you need to make a decision with it.
An SDK can connect to a custom endpoint, sign a request, and successfully upload a small object. That establishes only the basic request path. The bucket settings, addressing style, multipart flow, metadata, errors, and provider-specific behaviour your application relies on still need separate proof.
The useful question is which S3-shaped behaviours this application needs and which of them the implementation has proved.
That distinction matters for any local S3 server. It matters especially now that Amazon S3 itself covers more than one kind of bucket. AWS currently documents four bucket types: general purpose, directory, table, and vector buckets, each with different features and APIs. A local server can implement a useful subset of general-purpose object operations without implementing every newer S3 service. It has a narrower contract.
This checklist is for making that contract visible.
Disclosure: I maintain Objectively. I’m including it as a case study, not as an independent recommendation or a blanket compatibility guarantee.
First, separate three different kinds of compatibility
The word “compatible” hides at least three separate claims. Keep them separate.
Client compatibility
Can a client be configured to send requests to the endpoint?
AWS documents custom service endpoints for local and third-party development environments. Depending on the client, you might set an endpoint in code, in shared configuration, or with settings such as AWS_ENDPOINT_URL_S3. The AWS CLI also accepts --endpoint-url.
That is transport and configuration compatibility. It proves that the client can be pointed somewhere else. It doesn’t prove that the service at that address understands every request the client may make.
Protocol and operation compatibility
Does the endpoint understand the request, authenticate it, perform the operation, and return a response with the semantics your client expects?
This is where a useful checklist starts. “PutObject works” is one row. It isn’t a conclusion about GetObjectAttributes, a ranged read, multipart completion, bucket versioning, or a presigned request.
Service and operational compatibility
Does the environment have the properties of the service you deploy to?
Durability, availability, IAM policy evaluation, encryption, region behaviour, consistency under concurrency, network boundaries, quotas, event delivery, and failure modes all live here. AWS documents its S3 consistency model and data-protection model; a local endpoint can be excellent for exercising request and response code without providing those AWS production properties.
An SDK connection test covers the first layer. Your application acceptance tests need to cover the second. The real service, or a separately designed environment, still needs to cover the third.
Build the checklist in layers
Start with your application rather than an implementation’s feature page. Trace the S3 calls your code makes, then add the behaviours around those calls that could change the result.
1. Endpoint, addressing, and signing
Record:
- endpoint scheme: HTTP or HTTPS;
- host and port;
- path-style or virtual-hosted-style addressing;
- region used for the request and signature scope;
- credential type and rotation expectations;
- AWS Signature Version 4 header signing and query-string signing;
- proxy, IPv4/IPv6, and network-boundary assumptions.
This layer catches a common false positive. A client may be configured with http://localhost:9000, and the request may be signed correctly, while an application configured for virtual-hosted-style URLs still fails because the endpoint only parses the bucket from the path. AWS documents the difference between path-style and virtual-hosted-style requests.
2. Bucket operations and bucket families
List the bucket calls your application makes, such as create, list, head, delete, location, versioning, policy, CORS, lifecycle, encryption, tagging, or website configuration.
Then identify the bucket family involved. AWS’s current S3 documentation describes four families: general purpose, directory, table, and vector buckets. They are not interchangeable names for the same API surface. Directory buckets, S3 Tables, and S3 Vectors have their own namespaces, operations, and constraints.
For a normal local object-storage development loop, you may only need general-purpose bucket semantics. That is a valid scope. Write it down instead of allowing the word “S3” to imply the whole AWS catalogue.
3. Objects and metadata
For each object path in your application, check:
PutObject,GetObject,HeadObject, and delete behaviour;- object listing, prefixes, delimiters, pagination, continuation tokens, and ordering;
- copy operations;
- content type, cache headers, content disposition, language, and custom
x-amz-meta-*metadata; - ETags and conditional requests;
- range and suffix-range reads;
- response-header overrides;
- object keys containing spaces, Unicode, slashes, or characters that need encoding.
A browser showing an object is not proof that the same object can be retrieved with the headers and conditions your application uses. Keep the request and response details in the matrix.
4. Transfer patterns
Small uploads are the easy case. Include the transfer paths that matter to you:
- single-request uploads and downloads;
- multipart create, part upload, part copy, list, complete, and abort;
- retrying a part;
- completing the uploaded-parts list in ascending part-number order (parts may be uploaded in any order);
- presigned GET and PUT URLs;
- expiry and signed-header behaviour;
- large-object limits and memory or temporary-file behaviour.
If a client library silently switches to multipart above a size threshold, a successful small-object test can give you a misleadingly green result.
5. Authorisation and errors
Check the positive and negative paths. A local server that accepts one static credential pair may be perfectly adequate for an inner loop, whilst being nothing like an AWS account with IAM policies, bucket policies, ACLs, and multiple principals.
Capture the error code, HTTP status, response shape, and retry implications for at least one request that should fail. Useful negative tests include:
- an invalid or expired signature;
- a missing bucket or object;
- a non-empty bucket deletion;
- a malformed multipart completion;
- an operation that the local implementation deliberately does not support.
The error is part of the contract. An application may branch on NoSuchKey, AccessDenied, or InvalidRequest; a generic 500 response is not an equivalent result simply because both requests failed. AWS lists its S3 error responses separately from HTTP status handling.
6. Persistence, consistency, and operational boundaries
Finally, record the properties that aren’t visible in a single response:
- whether data survives an application restart;
- what “delete environment” or “reset” removes;
- read-after-write and list visibility under concurrent requests;
- event notifications;
- quotas, body-size limits, and multipart limits;
- TLS and network reachability;
- data-at-rest and credential handling;
- observability and request-log retention;
- whether the endpoint runs headlessly in CI.
These properties belong in a production decision. They answer a different question from whether an S3 SDK can issue a request.
Apply the checklist to Objectively
Objectively is designed for an interactive local S3 workflow on macOS. Its public product page presents a local endpoint, connection details, a request feed, and an object browser so you can inspect the requests and state produced by your application.
Use the checklist above before adopting it. Confirm the addressing mode and every bucket, object, metadata, multipart, presigned URL, and error path your application depends on. Keep IAM, cloud networking, durability, availability, billing, and other Amazon S3 behaviour in an AWS test environment.
Objectively is a desktop development tool, not a production object store or a general-purpose cloud emulator. If your application needs advanced bucket configuration, multiple AWS services, headless CI, or provider-specific behaviour, choose a test environment built for those requirements.
Turn an application trace into an acceptance test
You don’t need to test every operation AWS exposes. You do need to test every operation your application depends on, plus the edges most likely to change its behaviour.
A small acceptance worksheet might look like this:
| Test | What to record | Pass condition |
|---|---|---|
| Create and delete a bucket | request, region, response, empty/non-empty state | The response and follow-up state match the contract you need. |
| Put and get an object | key, content type, metadata, bytes | Returned bytes and relevant headers match. |
| Head an object | ETag, size, metadata, missing-object case | Both success and expected missing-object error are usable by the client. |
| List with a prefix | delimiter, pagination, continuation token | Keys and continuation behaviour match the application’s assumptions. |
| Conditional or range read | condition/range headers and response status | The client receives the status and byte range it expects. |
| Multipart upload | part sizes, order, completion, abort | The client can complete and clean up the upload. |
| Presigned operation | method, expiry, signed headers | The URL works for the intended method and stops working after expiry. |
| Negative control | an unsupported or deliberately invalid operation | The error code, status, and response shape are recorded rather than hand-waved. |
Run this worksheet against the local endpoint and then against AWS for the behaviours where provider-specific semantics matter. Record the product, client versions, date, test identifier, and relevant configuration so the result can be reproduced.
The matrix should use four labels:
- Supported: tested end to end in the recorded environment.
- Unsupported by design: an explicit, documented product boundary.
- Partially supported: some operations or conditions work, but the application needs a narrower statement.
- Not yet tested: no claim either way.
“Not yet tested” is a respectable result. It is much safer than turning an unverified implementation detail into a compatibility promise.
What local compatibility still can’t prove
A passing local matrix gives you confidence in the subset you tested. It does not prove AWS durability, availability, IAM evaluation, cloud networking, encryption at rest, billing behaviour, event delivery, region placement, or production-scale performance.
It also doesn’t prove that every version of every SDK will make the same requests. Client defaults change. New AWS API families appear. A dependency update can switch an addressing mode or introduce an operation your local endpoint doesn’t implement.
Treat compatibility as a dated contract. Re-run the matrix when the local product releases, when the SDK changes, when your application’s S3 trace changes, or when AWS adds a feature that affects your use case.
Where Objectively fits
Objectively is aimed at a focused Mac development loop: a local endpoint, a request feed, and an object browser for inspecting state. The useful product question is whether your required subset fits its current documented scope, not whether it replaces Amazon S3, a full cloud emulator, a deployable object store, or a headless test double.
If your application needs a focused local S3 loop on a supported Mac, inspect the current Objectively product page and test your required operations before adopting it. Keep a separate environment for IAM, TLS, virtual-hosted routing, advanced bucket families, CI, and other provider-specific behaviour.