docs
SQS compatibility
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.