A job queue where object storage is the only source of truth. Stateless compute. SQS-compatible. One Rust binary, one bucket.
01 / install
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"Without Docker: cargo build --release -p okuri-server produces a single okuri binary. Point OKURI_S3_* at your bucket and run okuri serve.
02 / semantics
| delivery | At-least-once. An acked message is never lost; an unacked one may be redelivered. |
| visibility | Timeouts 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 letters | Per-queue scope with manual redrive. |
| purge | Takes effect the moment it lands, without SQS's 60-second window. Messages sent after the purge are untouched by construction. |
| delay | Per-message, on send. |
| dedup | Producer-side window, in-memory and best-effort. It is not exactly-once, so consumers still need to be idempotent. |
| long polling | Up to 20 seconds per receive. |
| message size | 1 MB maximum, natively, with no need for payload-pointer workarounds. |
03 / sqs compatible
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.
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.
| divergence | detail |
|---|---|
| no FIFO queues | Ordering is best-effort until FIFO groups land. |
| manual redrive | An explicit operation through the native API; StartMessageMoveTask is not implemented. |
| 1 MB messages | Compared with SQS's 256 KB. |
| JSON 1.0 only | The legacy query protocol is refused; every current AWS SDK speaks JSON. |
04 / native api
One listener serves three surfaces from the same router:
| surface | port | auth | shape |
|---|---|---|---|
| SQS | :8080 | SigV4 | AWS JSON 1.0, existing SDKs unchanged |
| gRPC | :8080 | bearer key | okuri.api.v1.QueueService |
| HTTP/JSON | :8080 | bearer key | POST /v1/queues/{queue}/messages and friends |
05 / how it works
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.
06 / cost
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.
07 / when not to use
08 / correctness
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.