okuri

okuri

A job queue where object storage is the only source of truth. Stateless compute. SQS-compatible. One Rust binary, one bucket.

v0.0.6 · alpha · Apache-2.0· Rust

Bring up a full local stack, okuri plus MinIO:

git clone https://github.com/okuri-oss/okuri
cd okuri
docker compose up -d --wait

Then point any SQS client at it. The compose stack ships fixed development credentials, so the stock AWS CLI works as-is:

export AWS_ACCESS_KEY_ID=AKIDOKURICOMPOSEDEV
export AWS_SECRET_ACCESS_KEY=b2t1cmktY29tcG9zZS1kZXYtc3FzLXNlY3JldA==
export AWS_DEFAULT_REGION=us-east-1

QUEUE_URL=$(aws sqs create-queue --endpoint-url http://localhost:8080 \
    --queue-name jobs --query QueueUrl --output text)
aws sqs send-message --endpoint-url http://localhost:8080 \
    --queue-url "$QUEUE_URL" --message-body 'hello from okuri'
aws sqs receive-message --endpoint-url http://localhost:8080 \
    --queue-url "$QUEUE_URL"

The credentials above are fixed dev values baked into compose.yaml for the local stack only. Never reuse them anywhere real.

Without Docker: cargo build --release -p okuri-server produces a single okuri binary. Point OKURI_S3_* at your bucket and run okuri serve.

deliveryAt-least-once. An acked message is never lost; an unacked one may be redelivered.
visibilityTimeouts backed by leases. Every transition is fenced by compare-and-swap on the bucket, so a slow clock can cause a redelivery but can never lose or double-ack a message.
dead lettersPer-queue scope with manual redrive.
purgeTakes effect the moment it lands, without SQS's 60-second window. Messages sent after the purge are untouched by construction.
delayPer-message, on send.
dedupProducer-side window, in-memory and best-effort. It is not exactly-once, so consumers still need to be idempotent.
long pollingUp to 20 seconds per receive.
message size1 MB maximum, natively, with no need for payload-pointer workarounds.

okuri speaks AWS JSON 1.0 with SigV4 authentication, the same wire a current AWS SDK uses. All fourteen core queue and message actions are implemented; anything else answers UnsupportedOperation.

CreateQueue · GetQueueUrl · ListQueues · DeleteQueue · PurgeQueue · GetQueueAttributes · SetQueueAttributes · SendMessage · SendMessageBatch · ReceiveMessage · DeleteMessage · DeleteMessageBatch · ChangeMessageVisibility · ChangeMessageVisibilityBatch

Every CI run exercises the claim: AWS's own boto3 example runs unmodified against okuri, along with recorded boto3, aws-sdk-js, and aws-sdk-go sessions and live Celery and Laravel smoke tests.

divergencedetail
no FIFO queuesOrdering is best-effort until FIFO groups land.
manual redriveAn explicit operation through the native API; StartMessageMoveTask is not implemented.
1 MB messagesCompared with SQS's 256 KB.
JSON 1.0 onlyThe legacy query protocol is refused; every current AWS SDK speaks JSON.

Full divergence list in docs / sqs compatibility.

One listener serves three surfaces from the same router:

surfaceportauthshape
SQS:8080SigV4AWS JSON 1.0, existing SDKs unchanged
gRPC:8080bearer keyokuri.api.v1.QueueService
HTTP/JSON:8080bearer keyPOST /v1/queues/{queue}/messages and friends

A second listener on :9090 serves /healthz and /readyz for orchestrators.

Everything durable lives in the bucket:

Compute nodes hold no durable state at all. Kill a node mid-receive and another node picks up the lease when it expires. There is no cluster membership, no leader election for the data path, and no local disk to lose. Scaling out means starting another process against the same bucket.

Per-request object-store pricing is part of the design, so okuri publishes what a message costs. The table below comes from a seeded benchmark rather than an estimate, and just cost regenerates it:

                             per message produced          per message consumed
setting                  plain   cas write   get  list plain   cas write   get  list
linger 20ms               0.50  0.01  0.51  0.01  0.00  0.00  1.50  1.50 14.10  0.10
linger 100ms              0.10  0.01  0.11  0.01  0.00  0.00  1.10  1.10  3.70  0.10
linger 500ms              0.02  0.01  0.03  0.01  0.00  0.00  1.10  1.10  1.70  0.10

At $5.00 per million PUT-class requests and $0.40 per million GETs, the 100ms row totals $8.03 per million messages.

Most integration tests are deterministic simulations: a single-threaded runtime, a paused clock, seeded randomness. A failing test names its seed, and just dst <seed> replays it exactly.

CI holds a 90% line-coverage gate.