Skip to main content
On this page

Coverage by target cloud

How the targets compare

Each row compares a capability of SQS (FIFO) with its adaptation on each target. A dash means this row is not stated for that target.

Max adaptation

On Google Cloud

PostgreSQL hosting

Google operates Cloud SQL for PostgreSQL, including database backups and HA. The adapter uses short-lived Cloud SQL IAM tokens as database passwords, without an Auth Proxy or static password. Queue data stays outside the customer’s Kubernetes cluster and survives its rebuild. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain each message group before cutover to preserve order.

PostgreSQL queue behavior

Tensor9 adds an adapter, a proxy process, to your application pod and sets AWS_ENDPOINT_URL_SQS to its loopback address. Your application keeps its SendMessage and ReceiveMessage calls. The adapter implements them using PostgreSQL and returns SQS responses, error codes and MD5 checksums. Each queue has a durable PostgreSQL table with one row per message. SQL operations implement sending, receiving, deletion, delay, visibility, dead-letter delivery and counts. FIFO queues add ordering by message group, per-group sequence numbers and duplicate detection. Consumer redelivery remains possible.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from a durable Postgres table per queue.Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from a durable Postgres table per queue.

The application keeps its SQS SDK. The adapter stores each queue in a PostgreSQL table.

Architecture

The Rust adapter stores all durable state in PostgreSQL, including messages, claims and receipt handles. Instances can restart or scale out without a shared in-memory lock table, receiver pool or renewal loop. On Cloud SQL and Azure Flexible Server, it authenticates with short-lived Cloud SQL IAM or Microsoft Entra tokens generated in-process and passed as database passwords, without a static secret. On the Postgres side, the compiler provisions one database per appliance, a catalog entry per queue, and one durable table per declared queue. Each message is a row holding the SQS body and its attributes, its message-group, a visibility deadline, a receipt handle, a receive count, and a sent-at timestamp; the table is indexed so the hot paths stay a single scan. A FIFO queue uses the message group as the ordering unit; each row’s arrival position supplies its per-group sequence number. A dead-letter queue is another queue table, and a background retention reaper physically removes rows past their retention. CloudNativePG, Cloud SQL for PostgreSQL and Azure Flexible Server use the same adapter code and tables. Their operational difference is where data is stored: the managed servers are outside the appliance cluster, while CloudNativePG is inside it. See the rebuild limitation below.
The adapter stores messages, receipt handles, receive counts and visibility deadlines in PostgreSQL. Each declared queue has a table; a dead-letter queue is another table. Background cleanup enforces retention. FIFO rows also store a message group and sequence number; receives claim one message per group.The adapter stores messages, receipt handles, receive counts and visibility deadlines in PostgreSQL. Each declared queue has a table; a dead-letter queue is another table. Background cleanup enforces retention. FIFO rows also store a message group and sequence number; receives claim one message per group.

Each queue uses a PostgreSQL table, including dead-letter queues. The same adapter code runs against CloudNativePG, Cloud SQL for PostgreSQL and Azure Flexible Server.

Receiving messages

In one SQL operation, the adapter selects messages whose visibility deadlines have passed, in arrival order and up to the requested batch size. It assigns each a fresh receipt handle, advances its visibility deadline, increments the receive count and returns the claimed rows. Rows locked by another receiver are skipped. FIFO queues claim the next eligible message in each group, keeping one in flight per group. Visibility expiry permits redelivery; it does not stop a worker that is still processing an earlier receive. ApproximateReceiveCount is the message’s real receive count (the claim increments it; it is not estimated), and the SQS MD5-of-body and MD5-of-attributes are recomputed so your SDK verifies them byte-for-byte. DelaySeconds stamps the message’s visibility deadline at send, so a delayed message remains unavailable until that deadline. A long poll is a poll loop bounded by WaitTimeSeconds (capped at 20s), returning the instant a claim succeeds. A SendMessageBatch is a single multi-row insert with faithful per-entry outcomes, and the message body is stored in the queue table. The adapter defaults to a 262144-byte (256 KiB) queue limit; managed SQS requests enforce the declared limit before sending. Native AWS SQS now supports messages up to 1 MiB.
A receive claims messages in one atomic step: the adapter takes the head-of-queue messages that are due (their visibility deadline has passed), in arrival order, and in the same step assigns a fresh receipt handle, pushes the visibility deadline into the future, and increments the receive count, returning the claimed messages. Each receiver atomically claims a different batch of eligible messages. Messages can be redelivered when their visibility expires.A receive claims messages in one atomic step: the adapter takes the head-of-queue messages that are due (their visibility deadline has passed), in arrival order, and in the same step assigns a fresh receipt handle, pushes the visibility deadline into the future, and increments the receive count, returning the claimed messages. Each receiver atomically claims a different batch of eligible messages. Messages can be redelivered when their visibility expires.

Concurrent receives use atomic row claims and skip rows locked by another receiver. ApproximateReceiveCount is the message’s real receive count.

Receipt handles & visibility

Receiving writes a receipt token to the message row. DeleteMessage and ChangeMessageVisibility find that row by its token, including after an adapter restart or through another adapter instance. No in-memory receiver or broker lock is required. A delete or visibility change with no matching row returns ReceiptHandleIsInvalid. This includes messages already deleted or claimed again under a newer token. Visibility is a timestamp on the row, defaulting to 30 seconds on receive. ChangeMessageVisibility can extend it without a broker lock-duration maximum; zero makes the message immediately available to claim again. Retention can delete a message while it is in flight, and a newer claim makes the old token stale.
A receipt handle is a durable token stored on the message's own row, a stable id stamped on it, with no in-memory broker lock behind it, so it resolves the same after an adapter restart: Delete and ChangeMessageVisibility find the message again by that token. The visibility timeout is a timestamp on the row with no ceiling: ChangeMessageVisibility pushes it forward with no cap, where a broker-backed backend would stop at its lock ceiling.A receipt handle is a durable token stored on the message's own row, a stable id stamped on it, with no in-memory broker lock behind it, so it resolves the same after an adapter restart: Delete and ChangeMessageVisibility find the message again by that token. The visibility timeout is a timestamp on the row with no ceiling: ChangeMessageVisibility pushes it forward with no cap, where a broker-backed backend would stop at its lock ceiling.

Receipt handles are stored with messages, so they remain usable after an adapter restart. Visibility deadlines have no broker lock-duration limit.

Dead-letter & retention

During receive, one database transaction moves messages at or past maxReceiveCount from the source table to the dead-letter table and claims the remaining candidates. The move and claim commit together, so an interrupted transaction cannot leave a message deleted from the source without its DLQ copy. The DLQ uses the same queue operations and retention cleanup. Moved messages start with a reset receive count. A background cleanup task deletes messages whose send time is older than the queue’s retention period, including messages in flight. Receiving does not change that timestamp. Cleanup reads the stored retention attribute, defaults to four days and clamps it to 60 seconds–14 days. A missing or invalidly short value therefore cannot delete newly sent messages immediately. One worker per database runs each cleanup pass in batches, backs off during database maintenance and isolates queue errors. A circuit breaker and per-batch time limit bound the work.
Dead-letter and retention. A single atomic operation both claims live messages and redrives poison ones: from the locked head-of-queue set, messages at or past maxReceiveCount are moved from the source into the dead-letter queue and the rest are claimed and returned, in one transaction, so the move and claims commit together. This does not prevent consumer redelivery after visibility expiry. Separately, a background retention reaper removes messages whose send time is older than the queue's retention, clamped between 60 seconds and 14 days.Dead-letter and retention. A single atomic operation both claims live messages and redrives poison ones: from the locked head-of-queue set, messages at or past maxReceiveCount are moved from the source into the dead-letter queue and the rest are claimed and returned, in one transaction, so the move and claims commit together. This does not prevent consumer redelivery after visibility expiry. Separately, a background retention reaper removes messages whose send time is older than the queue's retention, clamped between 60 seconds and 14 days.

One transaction moves failed messages to the DLQ and claims eligible messages. A background worker removes messages older than their retention period, clamped to 60 seconds-14 days.

FIFO ordering & dedup

A FIFO queue stores the group stamped on each message, which is the ordering unit. Order within a group is a head-of-group claim: the receive takes the earliest message of each group that has nothing in flight (it confirms no message in that group is currently claimed and still invisible) and claims those heads atomically. Because each message’s arrival position is a monotonic identity, the earliest-per-group is exactly the group’s next in-order message, and exactly one message is in flight per group at a time. Groups are independent, so different receivers drain different groups concurrently, with Postgres as the only coordinator. An expired claim returns the same head to the front of its group, so a redelivery stays in sequence. Each FIFO send requires MessageGroupId and returns a SequenceNumber: the PostgreSQL-assigned arrival position formatted as a 20-digit, zero-padded number. FIFO deduplication uses a separate PostgreSQL table keyed by queue and deduplication ID. An explicit MessageDeduplicationId takes precedence; otherwise a queue configured for content-based deduplication uses SHA-256 of the body, excluding attributes. The message insert and deduplication marker are part of one transaction. A duplicate within 5 minutes rolls back its new message row and returns the original MessageId and SequenceNumber. Marker retention is independent of message retention, so receiving or deleting the original does not shorten the deduplication window. A claim prevents another receive in that group until deletion or visibility expiry. It does not stop a timed-out worker from continuing to process a previously received message; consumers must make repeated processing safe.
FIFO receives atomically claim the next message from each group that has no active claim. Each send returns a 20-digit, zero-padded sequence number. Explicit or content-based deduplication uses a separate PostgreSQL table with a five-minute window; repeated sends return the original message and sequence IDs.FIFO receives atomically claim the next message from each group that has no active claim. Each send returns a 20-digit, zero-padded sequence number. Explicit or content-based deduplication uses a separate PostgreSQL table with a five-minute window; repeated sends return the original message and sequence IDs.

FIFO receives claim one message per group at a time. Sends return a SequenceNumber and support a 5-minute deduplication window, using an explicit ID or a body hash when content-based deduplication is enabled.

Limitations

△ Where SQS and this backend diverge, read before you adopt
  • Throughput is bounded by one PostgreSQL server. All of an appliance’s queues share one PostgreSQL server, so peak throughput is bounded by that one server, and a heavy queue can affect its neighbors. On the Tensor9 round-trip eval the Postgres adapter holds throughput parity with native SQS (about 300 vs 286 ops/s) and a tighter end-to-end tail (p99 140 ms vs SQS’s 172 ms), while SQS is faster at the median (p50 15 ms vs 24 ms), results from that moderate-rate Standard-queue workload, not a measured throughput ceiling. This benchmark does not measure FIFO throughput or every hosting option.
  • CloudNativePG data lives in-cluster and does not survive a cluster rebuild. The in-cluster CloudNativePG host keeps queue data inside the appliance cluster, so a full cluster rebuild loses it; the two managed hosts (Cloud SQL Postgres, Azure Flexible Server) keep queue data on a provider-operated server outside the cluster, so it survives a rebuild. All three hosts use the same queue implementation; database placement changes rebuild recovery.
  • FIFO send deduplication lasts 5 minutes. A duplicate send in that window returns the original IDs. Reusing an ID after the window can create a new message. Consumer redelivery remains possible even within the window.
  • Stale receipt handles return an error. DeleteMessage and ChangeMessageVisibility return ReceiptHandleIsInvalid when no row matches the token.
  • Retention cleanup runs periodically. An expired message can remain until the next cleanup pass. Retention is clamped to 60 seconds–14 days, with invalidly short values raised to 60 seconds to avoid deleting newly sent messages.
  • Approximate counts are a point-in-time exact count. ApproximateNumberOfMessages and ...NotVisible are a real count of the visible and not-yet-visible messages in the queue’s table, exact at query time, but still Approximate under concurrent traffic; the delayed-count is reported as zero.
  • Control-plane calls operate on the apply-time schema. The database, the catalog, and each per-queue table (and its dead-letter table) are created by the compiler’s provisioner; runtime CreateQueue / DeleteQueue / SetQueueAttributes operate on that provisioned schema, and a switch between standard and FIFO is refused as a replace rather than silently mutating semantics.

Other considerations

  • Plan message cutover. The database, catalog and queue tables start empty. Existing SQS messages are not migrated. Drain each group before switching it to preserve order.
  • Provisioning and ownership. The adapter provisions and owns its own schema atomically per queue (one database per appliance, a catalog, and one durable table per declared queue, plus its dead-letter table), so a crash never leaves a half-provisioned queue. Each queue is marked with an ownership stamp: the layer never touches a queue it doesn’t own (an unstamped or foreign-owned queue detaches rather than deletes), and it refuses to drop a non-empty queue without force.
  • Three hosts, one backend, keyless on the managed ones. The same backend supports CloudNativePG, Cloud SQL Postgres and Azure Flexible Server. On the two managed hosts the connection is keyless (an Entra or Cloud SQL IAM token obtained in-process and presented as the database password, no static secret), and the provider operates the database’s durability, backup, and HA while Tensor9 operates the adapter.

On Azure, OCI, and Private Kubernetes

Via CloudNativePG

PostgreSQL hosting

Tensor9 operates CloudNativePG inside the customer’s Kubernetes cluster. Queue data is stored on that cluster’s volumes and does not survive a full cluster rebuild. Plan recovery and message cutover before replacing the cluster. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain each message group before cutover to preserve order.

Queue behavior

The adapter uses the same PostgreSQL tables, atomic message claims, stored receipt handles and transactional dead-letter moves described in PostgreSQL queue behavior. Receipt handles survive adapter restarts; stale claims return an error. Retention still removes messages in flight. All queues share database capacity, so a busy queue can affect others. The documented Standard-queue benchmark does not establish this host’s capacity or FIFO throughput. Duplicate sends are suppressed for five minutes; consumers must still handle redelivery.

On Azure

Via Azure Service Bus

How it works

Tensor9 adds an adapter, a proxy process, to your application pod and sets AWS_ENDPOINT_URL_SQS to its loopback address. Your application keeps its SendMessage and ReceiveMessage calls. The adapter translates them to Azure Service Bus operations and returns SQS responses, error codes and MD5 checksums. For a .fifo queue, Tensor9 provisions a Service Bus queue with sessions enabled. The adapter uses MessageGroupId as the session ID and selects session-based receiving from the queue’s .fifo suffix. Service Bus grants each session to one consumer at a time. The adapter delivers one message per group at a time, preserving send order across multiple consumer instances.
Before: on AWS the application's SQS SDK calls Amazon SQS FIFO. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS FIFO API from Azure Service Bus sessions.Before: on AWS the application's SQS SDK calls Amazon SQS FIFO. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS FIFO API from Azure Service Bus sessions.

The application keeps its SQS SDK. A Tensor9 adapter translates FIFO queue calls to Service Bus sessions.

Architecture

The Rust adapter translates SQS calls to Service Bus Standard over AMQP 1.0. On send, it sets the session ID from MessageGroupId. On receive, it delivers one message per group, renews the session lock during the visibility window and settles messages while preserving delivery counts. Authentication uses the customer’s AKS Workload Identity without a static secret. The adapter accepts Service Bus sessions independently of sending and standard-queue receiving, so a FIFO receive does not block those operations. The .fifo suffix selects this receive path; MessageGroupId supplies the session ID. The entity itself is provisioned at apply-time, session-enabled, before the first message arrives: sessions for the FIFO contract, broker-native duplicate detection over a 5-minute history window, a 5-minute session lock (giving the adapter’s lock renewal a 10x margin), expiry dead-lettering, and DLQ auto-forwarding. Microsoft operates Service Bus’s durability, backup, and HA; Tensor9 operates the adapter.
Architecture: the app's SQS SDK calls a Rust Tensor9 adapter over the pod loopback. The adapter serves the SQS FIFO API from an Azure Service Bus session-enabled queue whose session id is the MessageGroupId, giving strict per-group order with one message in flight per group across horizontally scaled consumers; FIFO session receives never block sends or standard receives.Architecture: the app's SQS SDK calls a Rust Tensor9 adapter over the pod loopback. The adapter serves the SQS FIFO API from an Azure Service Bus session-enabled queue whose session id is the MessageGroupId, giving strict per-group order with one message in flight per group across horizontally scaled consumers; FIFO session receives never block sends or standard receives.

A Rust adapter serves the SQS FIFO API in front of Service Bus; each session is one MessageGroupId , giving strict per-group order with one message in flight per group.

How sessions preserve group order

A Service Bus session groups messages under one ID and grants exclusive access to one consumer. While that consumer holds the session, other consumers cannot receive its messages. Messages are delivered in enqueue order; the adapter allows only one message in flight per group. Service Bus assigns each active group to one consumer even when several application pods receive from the same queue. Consumer pods therefore need no shared lock table, leader election or partition assignment to preserve group order.
  • One message in flight per group. A group’s head must be settled before its next message is delivered, the same one-in-flight contract as the memory and Postgres FIFO backends. This costs single-group throughput (one message per round-trip) in exchange for strict order.
  • The session lock keeps the group held. A held session is renewed by RenewSessionLock under your visibility window, so a slow consumer keeps its group rather than losing it mid-sequence; ChangeMessageVisibility extend maps to a lock renewal, and zero maps to an in-session immediate redelivery with order preserved.
  • Redelivery counts stay accurate. When the adapter releases a session, the in-flight message is abandoned first so its delivery count advances correctly, moving a poison message toward its redrive limit rather than being silently reset.
  • Receive counts reflect delivered messages. The adapter limits session receives to the remaining MaxNumberOfMessages allowance. It does not receive and discard extra messages that would increase their delivery count.
Sessions are the FIFO analog: each MessageGroupId is a broker session. The broker grants each session exclusively to one consumer, so two horizontally-scaled pods hold disjoint groups with one message in flight per group. Service Bus stores and coordinates session ownership; adapter instances do not share an in-memory lock table.Sessions are the FIFO analog: each MessageGroupId is a broker session. The broker grants each session exclusively to one consumer, so two horizontally-scaled pods hold disjoint groups with one message in flight per group. Service Bus stores and coordinates session ownership; adapter instances do not share an in-memory lock table.

Each MessageGroupId is a broker session; the broker hands each session to exactly one consumer, so ordering survives horizontal scale with no coordination between pods.

Duplicate detection

Service Bus duplicate detection uses a 5-minute history window. Sends with the same deduplication key within that window create one queued message. A consumer can still receive that message again after visibility expires. The broker stores the deduplication state; the adapter needs no separate reconciliation loop. An explicit MessageDeduplicationId takes precedence. Otherwise, the adapter uses a hash of the body for every .fifo send. It always sets this hash because its messaging permissions do not allow it to read the queue’s deduplication setting. With duplicate detection enabled, Service Bus uses the hash to suppress retries; with detection disabled, it is only an ID. The adapter separately stores the SQS MessageId as an application property and returns it on receive. Equal-body messages sent outside the deduplication window retain distinct SQS IDs.
Duplicate detection: on a .fifo send the adapter sets the dedup key from an explicit MessageDeduplicationId, else a content hash of the body. The queue is provisioned with broker-native duplicate detection over a 5-minute window, so a retried equal-body send is suppressed by the broker without a second enqueue; consumers can still receive a message again after visibility expires; the SQS MessageId is preserved as an application property.Duplicate detection: on a .fifo send the adapter sets the dedup key from an explicit MessageDeduplicationId, else a content hash of the body. The queue is provisioned with broker-native duplicate detection over a 5-minute window, so a retried equal-body send is suppressed by the broker without a second enqueue; consumers can still receive a message again after visibility expires; the SQS MessageId is preserved as an application property.

The adapter uses an explicit MessageDeduplicationId or a body hash as the Service Bus message ID. The broker suppresses repeated IDs within 5 minutes.

Dead-lettering

Service Bus dead-letters a message when its delivery count reaches the limit set by maxReceiveCount. The next message in that group can then be delivered. The adapter cannot receive from Service Bus’s internal dead-letter subqueue. Tensor9 therefore configures automatic forwarding to your declared dead-letter queue (DLQ), preserving the session ID. A FIFO source requires a FIFO DLQ, which is provisioned with sessions enabled. Messages whose time to live (TTL) expires are also dead-lettered, so they can be inspected or recovered from the DLQ. The pairing rule is enforced at compile time: a non-.fifo DLQ declared on a .fifo origin stops the build with a clear error rather than compiling a queue whose dead letters would have nowhere valid to land.
Dead-lettering: a poison message that fails delivery maxReceiveCount times is dead-lettered by the broker and its group advances; the dead letter auto-forwards to your declared, session-enabled DLQ, retaining its session id, and TTL-expired messages land there too.Dead-lettering: a poison message that fails delivery maxReceiveCount times is dead-lettered by the broker and its group advances; the dead letter auto-forwards to your declared, session-enabled DLQ, retaining its session id, and TTL-expired messages land there too.

Service Bus forwards messages exceeding maxReceiveCount to the declared DLQ, which the adapter can receive from.

Limitations

△ Where SQS FIFO and Service Bus diverge, read before you adopt
  • A dedup-suppressed send returns a fresh MessageId. SQS echoes the original message’s MessageId (and SequenceNumber) when it suppresses a duplicate; here the suppressed send returns a fresh MessageId that corresponds to no delivered message. The duplicate enqueue is still suppressed (consumer redelivery remains possible), but a producer that correlates on the duplicate’s echoed id must not wait on it.
  • Send responses omit SequenceNumber. Service Bus cannot return its sequence number synchronously on send. Receive responses include the broker-assigned number, which increases across the queue and therefore within each group.
  • The visibility timeout is capped at 5 minutes. A visibility timeout maps to the session lock, whose ceiling is the 5-minute session lock the compiler sets (a 10x margin over the adapter’s lock renewal), so a requested visibility above 5 minutes is rejected with an error rather than being silently clamped. Extension via ChangeMessageVisibility renews the lock.
  • Receipt handles expire on adapter restart. A handle requires a live session lock. After a restart, old handles are invalid and the group’s in-flight message is delivered again in order when its session lock expires, within 5 minutes.
  • Expiry can affect a whole session. SQS retention applies per message. On Service Bus, an expired message at the front can expire its entire session. The expired messages are dead-lettered and can be recovered from the DLQ, but more messages may expire together than on SQS.
  • Single-group throughput is one message per round-trip. The one-in-flight strictness that guarantees per-group order costs single-group throughput: one message per round-trip per group, versus up to 10 per receive batch on SQS. Total throughput scales with the number of concurrent groups, so parallelism comes from having many groups, not from batching within one.
  • An empty-queue receive can block past the requested wait. SQS returns within WaitTimeSeconds . A Service Bus session accept is broker-controlled and is never cancelled mid-attach (a cancelled accept could strand a broker-side session lock), so a FIFO receive on an empty queue can overrun the requested wait by up to one accept window, about 60 seconds worst case (the exact figure on real Azure is reported by your own monitoring once this is running).
  • Per-message DelaySeconds is rejected. A per-message delay is rejected exactly as real SQS rejects it on a FIFO queue: a scheduled enqueue would activate after later siblings and break in-group order. Queue-level policy changes have a separate scope: Max manages logical queue settings, but the messaging backend rejects nonzero queue delay and new dead-letter routes.
  • Changing FIFO or deduplication settings replaces the queue. Sessions and duplicate detection are fixed at queue creation. Changing standard/FIFO mode or content-based deduplication destroys and recreates the queue, losing queued messages. Every FIFO compilation includes a warning about replacement. Drain the queue before changing these settings.
  • A send without a deduplication ID can be accepted when SQS would reject it. SQS requires MessageDeduplicationId when content-based deduplication is disabled. The adapter cannot read that queue setting with its messaging permissions, so it accepts the send. Group ordering is preserved, but the SQS validation error is not reproduced.

Other considerations

  • Data migration. The Service Bus namespace and session-enabled queue are provisioned empty at apply-time; in-flight SQS messages are not migrated. Cut over at a drain point (or dual-write) so the new queue starts clean; for FIFO, drain per group so no group cuts over mid-sequence.
  • Queue configuration is applied during provisioning. Each queue gets a 5-minute session lock, ten times the adapter’s renewal interval. The declared maxReceiveCount determines dead-letter forwarding to the DLQ. Runtime message operations require no queue-management credential.
  • Operations and ownership. The adapter provisions the Service Bus deployment itself: the namespace and the session-enabled queue (with duplicate detection, a 5-minute lock, expiry dead-lettering, and DLQ auto-forwarding), all derived from your SQS queues’ declared shapes; the adapter is injected alongside the application. Microsoft operates Service Bus (Standard): durability, backup, and HA. What remains operational is the surrounding platform: the Azure subscription, cluster patching, and monitoring.
  • FIFO requires an explicit selection. Older deployments that used the unsplit SQS-to-Service-Bus target supported standard queues only. Tensor9 does not convert those deployments to FIFO. A FIFO queue must be declared and selected explicitly to use sessions.

Via Azure Cosmos DB (queue)

How it works

Tensor9 adds an adapter, a proxy process, to your application pod and sets AWS_ENDPOINT_URL_SQS to its loopback address. Your application keeps its SendMessage and ReceiveMessage calls. The adapter implements them using Cosmos DB and returns SQS responses, error codes and MD5 checksums. Each queue has a Cosmos DB container, with one document per message. The adapter uses these documents for sending, receiving, deletion, delay, visibility, dead-letter delivery and counts. FIFO queues add ordering by message group and duplicate detection, described below.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Cosmos DB.Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Cosmos DB.

The application keeps its SQS SDK. The adapter stores and reads messages in Azure Cosmos DB.

Architecture

The Rust adapter stores durable state in Cosmos DB, so instances can restart or scale out without sharing an in-memory lock table. It authenticates through the customer’s AKS Workload Identity without a static secret. Microsoft operates Cosmos DB’s durability, backups and high availability; Tensor9 operates the adapter. On the Cosmos side, the compiler provisions one serverless Cosmos account and one database per appliance , and inside it one container per declared queue (the container id is the queue name). Every message is a document that holds the SQS body, its attributes, its receive count, and a visibility timestamp. A Standard queue stores its messages durably and claims eligible messages in arrival order. Concurrent claims do not establish a fairness or no-starvation guarantee. FIFO queues order by MessageGroupId, with one message in flight per group, as described in the FIFO section. Retention rides Cosmos’s native per-item TTL, so an expired message is reclaimed by Cosmos itself.
  • Cosmos DB coordinates message claims. Adapter instances share message and claim records in the database, so an adapter restart does not erase them.
The adapter serves SQS requests using Azure Cosmos DB. One serverless account and database hold one container per declared queue. Cosmos stores messages and coordinates claims. FIFO queues keep strict per-group order with one message in flight per group.The adapter serves SQS requests using Azure Cosmos DB. One serverless account and database hold one container per declared queue. Cosmos stores messages and coordinates claims. FIFO queues keep strict per-group order with one message in flight per group.

A queue is one Cosmos container and each message is a document; a FIFO queue keeps strict per-group order with one message in flight per group.

Receiving messages

Receiving atomically claims the oldest visible message, hides it from other receivers and increments its receive count. If another receiver claims the candidate first, the adapter moves to the next one. Each successful conditional write has one owner; visibility expiry still permits redelivery. FIFO queues select the next sequence position within each message group and allow one message in flight per group. Messages can be redelivered after visibility expires. ApproximateReceiveCount is the real receive count (incremented on each claim, not estimated), and the SQS MD5-of-body and MD5-of-attributes are recomputed so your SDK verifies them byte-for-byte. FIFO rejects per-message DelaySeconds, including zero; omit the parameter. A SendMessageBatch runs as per-entry writes with faithful per-entry outcomes.
A conditional write claims an eligible message. A competing receiver that loses the claim tries another candidate. Visibility expiry can cause redelivery.A conditional write claims an eligible message. A competing receiver that loses the claim tries another candidate. Visibility expiry can cause redelivery.

A receive reads the oldest visible message and claims it for exactly one consumer; if another consumer claims it first, this receive moves to the next candidate. ApproximateReceiveCount is the real receive count, incremented on each claim, never estimated.

Receipt handles & visibility

SQS hands your consumer a receipt handle to delete or extend a message. Here that handle is a token for claim state stored in Cosmos DB. Because the adapter keeps no in-memory lock table, a current handle resolves the same way before and after a restart: DeleteMessage and ChangeMessageVisibility keep working. A stale handle (the message was already deleted, or a newer claim superseded this one) is a no-op on delete, exactly as SQS’s own stale handle is; a cross-queue or unparseable handle is rejected as ReceiptHandleIsInvalid. ChangeMessageVisibility updates the stored visibility time without a broker lock-duration maximum. Zero makes the message immediately available to claim again. Retention can expire a message while it is in flight; extending visibility does not extend its lifetime. A stale handle is rejected, allowing the consumer to detect that it no longer owns the claim. Visibility expiry does not stop a worker from continuing to process an earlier receive.
A receipt handle is plain data with no broker lock behind it, so it resolves the same after the adapter restarts. The visibility timeout has no ceiling: ChangeMessageVisibility extends it forward with no cap, where a broker-backed backend would stop at its lock ceiling.A receipt handle is plain data with no broker lock behind it, so it resolves the same after the adapter restarts. The visibility timeout has no ceiling: ChangeMessageVisibility extends it forward with no cap, where a broker-backed backend would stop at its lock ceiling.

Receipt handles remain usable after an adapter restart. Visibility is stored in Cosmos DB and has no broker lock-duration limit.

FIFO ordering & dedup

FIFO queues order messages by MessageGroupId and support duplicate detection. Standard queues have neither feature. Cosmos DB assigns each message a group position before enqueueing it. Concurrent senders therefore share one order without relying on their clocks. Each FIFO send returns that position as SequenceNumber. Sequence numbers increase but need not be consecutive; a gap does not stop the group. Receiving claims the next message from a group only if no message in that group is already in flight. The check and claim preserve this rule across concurrent receivers, including when a slow send arrives late. Different groups can be processed concurrently without coordination between consumers. When a claim times out, delivery resumes from the same message in its group. Within the 5-minute deduplication window, a repeated send is suppressed and returns the original MessageId and SequenceNumber, including after a crash during the send. The key is an explicit MessageDeduplicationId, or a hash of the body when content-based deduplication is enabled. A FIFO send with neither returns InvalidParameterValue. Standard queues reject MessageDeduplicationId because they do not support deduplication.
  • Sending and receiving use separate tracking. Sequence assignment does not block the group-claim mechanism used by receivers, so producers and consumers can proceed independently.
  • Clock skew can shorten visibility. The one-message-per-group claim check does not use a clock. Visibility checks do, so clock differences between consumers can cause early redelivery without allowing two active claims in the same group.
FIFO on Cosmos: each group is delivered in strict send order with exactly one message in flight per group, correct across horizontally scaled consumers; duplicate sends are suppressed and echo the original MessageId and SequenceNumber.FIFO on Cosmos: each group is delivered in strict send order with exactly one message in flight per group, correct across horizontally scaled consumers; duplicate sends are suppressed and echo the original MessageId and SequenceNumber.

Order within a group is strict send order; exactly one message is in flight per group; duplicate sends are suppressed and echo the original MessageId and SequenceNumber. This section applies to FIFO queues only.

Dead-letter queues

A message received more than maxReceiveCount times is moved to the declared dead-letter queue. If the adapter crashes during the move, retry ensures it reaches the DLQ before removal from the source. A retry can produce a duplicate in the DLQ; consumers there must handle at-least-once delivery. The adapter moves a message that exceeded its receive limit to the DLQ before claiming another message in the group. The moved message retains its group and order in the FIFO DLQ, with its receive count reset to 0. If a message expires under retention instead, it is removed and the next message becomes available in the group.
A failed message is copied to its dead-letter queue and then removed from the source. If the adapter crashes between those steps, retry completes the move; the dead-letter queue can contain a duplicate.A failed message is copied to its dead-letter queue and then removed from the source. If the adapter crashes between those steps, retry completes the move; the dead-letter queue can contain a duplicate.

The adapter retries interrupted dead-letter moves. Delivery to the DLQ is at-least-once, so a crash can cause a duplicate.

Limitations

△ Where SQS and Cosmos diverge, read before you adopt
  • Approximate counts can lag by about 5 seconds. The adapter caches counts briefly, so recent sends, settlements and expirations may not be reflected immediately. Use Azure monitoring to observe counts after deployment.
  • Dead-letter delivery can produce duplicates. Retry completes a move interrupted by an adapter crash without losing the message, but can deliver it to the DLQ more than once.
  • Queue capacity is bounded by the serverless container count. All of an appliance’s queues share one serverless Cosmos account, which caps at about 500 containers, so roughly 499 queues per appliance (one container per queue). Beyond that the build stops with a clear error rather than half-provisioning. The account is serverless by design, a fit for idle-heavy queue fleets.
  • Single region. The account is single-region (the appliance’s region); there is no geo-replica.
  • Clock skew can shorten FIFO visibility. The group claim check is independent of clocks, but visibility uses wall-clock time. Clock differences between consumers can cause early redelivery while preserving one active claim per group.
  • Cosmos DB removes expired messages. Per-item time to live (TTL) is enforced by background cleanup. Expiry can occur while a consumer is processing the message. Removing an expired first message allows the next message in its group to proceed.
  • Per-message delay is rejected. FIFO queues reject a supplied DelaySeconds, including zero. Omit it from individual messages.
  • Separate logical queue management from container provisioning. Max manages logical queues on provisioned containers, listings, tags and supported defaults, including retention. Terraform owns the account, database and containers; deleting a logical queue does not delete its container. Nonzero queue-level delay and new dead-letter routes are rejected.

Other considerations

  • Data migration. The Cosmos account, database, and per-queue containers are provisioned empty at apply-time. In-flight SQS messages are not migrated; drain each group before cutover so no group switches mid-sequence.
  • Operations and ownership. The adapter provisions the Cosmos deployment itself (one serverless account, one database per appliance, and one container per declared queue), all derived from your declared queues; the adapter is injected alongside the application. Microsoft operates Cosmos’s durability, backup, and HA; Tensor9 operates the adapter. What remains operational for the deployment is the surrounding platform: the Azure subscription, cluster patching, and monitoring.
  • Monitor database usage. Use Azure monitoring to track Cosmos DB request units and latency; queue counts are cached for about 5 seconds.
  • Retention is native, whole-second Cosmos TTL. A message’s remaining lifetime is Cosmos’s native per-item TTL set to the remaining whole-second budget (floored at one second, so Cosmos reclaims at or after the message is hidden, never before it), with no background cleanup of its own.

Via PostgreSQL Flexible Server

PostgreSQL hosting

Microsoft operates PostgreSQL Flexible Server, including zone-redundant HA and point-in-time recovery backups. The adapter uses short-lived Microsoft Entra tokens as database passwords. Queue data stays outside the customer’s Kubernetes cluster and survives its rebuild. One server and database hold all queues for the deployment, sharing capacity and maintenance; the default server size is fixed because SQS supplies no source instance size. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain each message group before cutover to preserve order.

Queue behavior

The adapter uses the same PostgreSQL tables, atomic message claims, stored receipt handles and transactional dead-letter moves described in PostgreSQL queue behavior. Receipt handles survive adapter restarts; stale claims return an error. Retention still removes messages in flight. All queues share database capacity, so a busy queue can affect others. The documented Standard-queue benchmark does not establish this host’s capacity or FIFO throughput. Duplicate sends are suppressed for five minutes; consumers must still handle redelivery. Service Catalog.