# SQS compatibility

> The fourteen implemented actions, the queue URL model, and every documented divergence from SQS.

okuri speaks the AWS JSON 1.0 protocol with SigV4 authentication, the same
wire a current AWS SDK uses. Pointing an SDK at okuri means changing its
endpoint and nothing else.

Compatibility is checked on every CI run: AWS's own boto3 example code runs
unmodified against okuri (vendored byte-for-byte and SHA-256 pinned), along
with recorded boto3, aws-sdk-js, and aws-sdk-go sessions and live Celery
and Laravel smoke tests.

## Implemented actions

All fourteen core queue and message actions:

| group | actions |
| --- | --- |
| queue lifecycle | `CreateQueue`, `GetQueueUrl`, `ListQueues`, `DeleteQueue`, `PurgeQueue` |
| attributes | `GetQueueAttributes`, `SetQueueAttributes` |
| send | `SendMessage`, `SendMessageBatch` |
| receive | `ReceiveMessage` |
| delete | `DeleteMessage`, `DeleteMessageBatch` |
| visibility | `ChangeMessageVisibility`, `ChangeMessageVisibilityBatch` |

Every action outside this table answers `UnsupportedOperation`. That is the
whole rule: the surface AWS ships keeps growing, and an endpoint that
enumerated its refusals would be wrong the next time it grew.

## Divergences

Each entry states what a migrating client observes first and the reason
second. These are documented boundaries rather than a defect list, and
nothing here is a promise to close a gap later.

**Clients on a pre-2023 SDK cannot connect at all.** The endpoint speaks
AWS JSON 1.0 only, so an SDK older than the November 2023 protocol
crossover sends the legacy query protocol and is refused. The only fix is
upgrading the SDK; there is no server setting for it. Two known populations: aws-sdk-js
v2 (never received JSON support, out of support since 2025-09-08), and
Celery deployments still carrying the widely copied 2023 workaround that
pinned botocore at 1.31.80. Lift the pin.

**A queue name that works against AWS may be rejected here.** Uppercase
letters, names of 65 to 80 bytes, and any `.fifo` suffix are refused rather
than folded, because a name reaches an object key. Provisioning
`OrdersQueue` fails at create time.

**FIFO queues do not exist.** There is no ordering guarantee, message
grouping, content-based deduplication, sequence numbering, or `.fifo`
naming. An
application that depends on per-group ordering does not port; one that only
used FIFO for deduplication should look at the native API's idempotent
produce, which is best-effort and documented as such.

**A queue's dead-letter queue is fixed and unshareable.** Exactly one per
queue, addressable as `{queue}-dlq`, and `RedrivePolicy` may name no other
target. A fleet sharing one dead-letter queue does not port.
`RedriveAllowPolicy` is refused. The `-dlq` suffix is reserved.

**There is no message-move task.** `StartMessageMoveTask` is not
implemented. Draining a dead-letter queue back to its parent is a
native-API operation, and the dead-letter queue is receivable directly
under its own name in the meantime.

**A nack cannot carry a backoff.** Returning a message early is
`ChangeMessageVisibility` with a timeout of zero, which SQS clients already
do. The native API's optional per-nack backoff has no expression in the SQS
surface.

**In-flight and delayed counts are absent, not zero.**
`ApproximateNumberOfMessagesNotVisible` and
`ApproximateNumberOfMessagesDelayed` are omitted from `GetQueueAttributes`,
because separating those cases would mean reading every lease object per
call. An autoscaler keyed on in-flight depth has to key on
`ApproximateNumberOfMessages` instead. `CreatedTimestamp` is likewise
absent.

**Queue-level `DelaySeconds` and `ReceiveMessageWaitTimeSeconds` are always
zero.** Both are accepted when set to zero, so provisioning templates do
not fail, and refused at any other value. Per-message `DelaySeconds` on
send and per-request `WaitTimeSeconds` on receive are fully supported; only the
queue-level defaults have nowhere durable to live. The best-known affected
client is Laravel's queue driver, whose `pop` sends no `WaitTimeSeconds`:
against okuri it short-polls on its configured sleep interval, a cost
difference rather than a correctness one.

**A long poll may be shorter than the client asked for.** `WaitTimeSeconds`
is bounded at SQS's 20 seconds. The server's own long-poll maximum is
deployment configuration defaulting to 20; a wait above it is clamped
rather than refused, and nothing on the wire says the wait was shortened.

**There is no IAM and no access policy.** `Policy`, `AddPermission`, and
`RemovePermission` are refused rather than accepted and ignored, because an
ignored policy is a security claim that is not true. Authorization is a
static credential scoped to one namespace.

**A client cannot set encryption on a queue.** `KmsMasterKeyId`,
`KmsDataKeyReusePeriodSeconds`, and `SqsManagedSseEnabled` are refused.
Encryption is the object store's, configured once by the operator; a
per-queue setting would report a protection that may not exist.

**Tags are not implemented.** `TagQueue`, `UntagQueue`, and
`ListQueueTags` answer `UnsupportedOperation`, as does
`ListDeadLetterSourceQueues`.

**Message system attributes are not accepted on send.** `AWSTraceHeader` is
refused rather than stored, so an X-Ray-instrumented client's send fails
rather than quietly dropping the header.

**`SenderId` and `ApproximateFirstReceiveTimestamp` are absent on
receive.** No sender identity is recorded, and the lease ledger counts
deliveries without stamping the first.

**Retention default depends on which surface created the queue.** A queue
created through the SQS endpoint without `MessageRetentionPeriod` gets
SQS's four days. A queue created through the native API keeps the library's
unbounded default. Reading either through either surface reports what was
actually written.

**Receipt handles are a different shape from AWS's.** Opaque and
round-trippable, well inside the 1024-character bound, but a different
length and character set. Code that parsed a handle was already outside
AWS's contract.

**Purge is stronger than SQS's.** It is immediate and exact where SQS's is
eventual within 60 seconds. A message sent after the cut can never be
deleted, and there is no `PurgeQueueInProgress` error because there is no
window to be inside. The response also carries one member AWS's does not:
`PurgedBeforeUnixMs`, the instant the cut was applied. SDKs that do not
know the member ignore it.

**Queue deletion is stronger than SQS's.** Effective when the CAS lands
rather than within 60 seconds, and the name is immediately reusable.

**There is no in-flight message cap.** SQS's 120,000 in-flight limit and
its `OverLimit` error have no equivalent.

**A body SQS would reject is accepted here.** SQS restricts a body to a
subset of Unicode; okuri stores the payload uninterpreted, so every body an
SQS client can send works, read back exactly as sent.

**`LastModifiedTimestamp` is the bucket's clock, not okuri's.** The value
is the manifest object's last-modified metadata reported straight through.
Using it to notice that a queue's configuration changed is fine; using it
to order queue changes against message timestamps is comparing two clocks.

**A compressed answer is never returned.** The endpoint always answers with
identity encoding, whatever `accept-encoding` the client offers.
