Skip to main content
Amazon SQS
Amazon SQS is a trademark of its respective owner, used here for identification and migration comparison only. No endorsement, partnership, or affiliation is implied.
Amazon SQS is the default durable message queue: create a queue, SendMessage, long-poll with ReceiveMessage, DeleteMessage when you’re done, and get at-least-once delivery with retries and dead-letter queues. It’s battle-tested and the SDK is mature. But SQS is also a raw slice of an AWS account that sits apart from everything else your agent touches:
  • Every queue lives behind IAM credentials (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY or a role), an AWS account, and CloudWatch — disconnected from wherever the agent’s email, cards, secrets, and KYC live.
  • The credential is coarse: a key with sqs:* (or even a scoped policy) is an account-level grant. There is no per-end-user queue — you build tenant isolation yourself with naming conventions and hand-written IAM policies.
  • “Which customer’s agent enqueued that job, and what else can this agent touch?” is answered in IAM + CloudWatch for queues, and in unrelated systems for everything else. The queue has no shared accountability with the rest of the agent’s footprint.
Naive’s queue primitive gives the agent the same capability — a durable, at-least-once work queue (it’s Amazon SQS underneath) — but the queue is rooted in one governed identity:
  • The tenant user that owns the queue is the same user that owns its email inboxes, its cards, its vault secrets, and its KYC.
  • The agent never holds a cloud key. Naive owns the underlying cloud credentials and scopes every queue to your tenant (one queue per resource, namespaced + tagged, with a queue policy locked to your tenant’s task role) — the same model as Compute.
  • Whether an agent may use the queue at all is decided by that user’s Account Kit, and every request is metered against the tenant’s plan — so a leaked agent key can’t quietly run up a bill on your raw AWS account.
This guide maps SQS’s queue model to Naive’s queue primitive, shows the smallest working swap, and is explicit about the SQS features that don’t map yet.
Tested against: the AWS SDK for JavaScript v3 @aws-sdk/client-sqs v3.1080.x (SQSClient, CreateQueueCommand, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand, GetQueueAttributesCommand, PurgeQueueCommand, DeleteQueueCommand; Query/JSON protocol against https://sqs.<region>.amazonaws.com) and the Naive Node SDK @usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1, docs snapshot July 2026).Version notes:
  • SQS has two queue types: standard (best-effort ordering, at-least-once) and FIFO (queue name ends .fifo, exactly-once, ordered per MessageGroupId). Naive maps both: create({ type: "standard" }) and create({ type: "fifo" }).
  • SQS dead-letter queues are configured with a RedrivePolicy attribute pointing at a second queue’s ARN and a maxReceiveCount. Naive maps the DLQ with a single flag: create({ dlq: true }) — but maxReceiveCount and redrive config are not exposed. See what doesn’t map yet.
  • SQS identifies a queue by its QueueUrl (per region/account). Naive identifies a queue by its id and resolves the region/account for you.
  • On Naive the queue is per-tenant: naive.queue.* acts as the account’s default agent profile, naive.forUser(id).queue.* is a specific end-user. There is no SQS equivalent — tenant isolation in SQS is naming + IAM you write yourself.

Concept map

Amazon SQSNaiveNotes
SQSClient behind AWS_ACCESS_KEY_ID / IAM role — one AWS account, shared across tenantsnaive.forUser(id).queue — scopes every primitive per end-userThe core consolidation win
IAM credentials held by your code; the queue runs on your AWS trustAgent never holds a cloud key — Naive owns the AWS creds, scopes to your tenantSame model as Compute
CreateQueueCommand({ QueueName }){ QueueUrl }naive.queue.create({ name }){ id }Standard queue
CreateQueueCommand({ QueueName: "x.fifo", Attributes: { FifoQueue: "true" } })naive.queue.create({ name, type: "fifo" })FIFO queue
RedrivePolicy attribute → separate DLQ + maxReceiveCountnaive.queue.create({ name, dlq: true })DLQ provisioned for you; maxReceiveCount not exposed — see gaps
SendMessageCommand({ QueueUrl, MessageBody }){ MessageId }naive.queue.send(id, body)Body is a string on both
SendMessageCommand({ ..., MessageGroupId, MessageDeduplicationId })naive.queue.send(id, body, { group_id, dedup_id })FIFO ordering + dedup
SendMessageCommand({ ..., DelaySeconds })naive.queue.send(id, body, { delay_seconds })Per-message delay
ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages, WaitTimeSeconds }){ Messages }naive.queue.receive(id, { max, wait }){ messages }Long-poll; max ≤ 10, wait ≤ 20s
ReceiveMessageCommand({ ..., VisibilityTimeout })naive.queue.receive(id, { visibility })In-flight visibility window
message.Body / message.ReceiptHandlem.body / m.receipt_handlePer-message fields
DeleteMessageCommand({ QueueUrl, ReceiptHandle })naive.queue.ack(id, receipt_handle)Ack = delete after processing
GetQueueAttributesCommandApproximateNumberOfMessages etc.naive.queue.attributes(id)Approximate depth / in-flight / delayed
PurgeQueueCommand({ QueueUrl })naive.queue.purge(id)Drop all messages
DeleteQueueCommand({ QueueUrl })naive.queue.destroy(id)Delete the queue (and its DLQ)
ListQueuesCommand(){ QueueUrls }naive.queue.list()Queues for the identity
GetQueueUrlCommand({ QueueName })naive.queue.get(id)Fetch one queue
MessageAttributes (typed key/value map alongside the body)(none)Encode metadata in the body — see gaps
SendMessageBatchCommand / DeleteMessageBatchCommand (≤10 per call)(none)Loop send / ack — see gaps
ChangeMessageVisibilityCommand (extend a single in-flight message)(none)Set visibility on receive; no per-message extend — see gaps
SetQueueAttributes (retention, max size, KMS SSE, redrive tuning)(managed)Naive sets sane defaults; not tunable — see gaps
IAM policy / access policy scopes SQS in one AWS accountAccount Kit gates queue per user + plan quota meters every callGovernance on the identity — see gains
CloudWatch metrics/logs for the queuenaive.forUser(id).logs.query() — one per-user timelineQueue events beside email, cards, vault — see gain #3
AWS CLI aws sqs send-messagenaive queue send <id> '{...}'Full CLI parity — see CLI

Before / after: the core path

The path that matters for almost every queue-backed agent is create a queue, enqueue work, long-poll for it, process, then delete the message. Here it is on both platforms.
import {
  SQSClient, CreateQueueCommand, SendMessageCommand,
  ReceiveMessageCommand, DeleteMessageCommand,
} from "@aws-sdk/client-sqs";

// Needs AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or a role) + a region
const sqs = new SQSClient({ region: "us-east-1" });

// Create the queue (account-level; you namespace + IAM-scope tenants yourself)
const { QueueUrl } = await sqs.send(new CreateQueueCommand({ QueueName: "jobs" }));

// Producer
await sqs.send(new SendMessageCommand({
  QueueUrl,
  MessageBody: JSON.stringify({ task: "resize", url: "https://..." }),
}));

// Consumer — long-poll, process, then delete each message
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
  QueueUrl, MaxNumberOfMessages: 10, WaitTimeSeconds: 20,
}));
for (const m of Messages) {
  // ...process m.Body...
  await sqs.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle: m.ReceiptHandle }));
}
// → the queue is a slice of your AWS account behind an IAM key. No per-user
//   isolation, and no shared identity with the agent's email, cards, or vault.
import { Naive } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
const client = naive.forUser("acme"); // same id space as your own users

// Create the queue — scoped to this end-user, no cloud key in the agent
const q = await client.queue.create({ name: "jobs" });

// Producer
await client.queue.send(q.id, JSON.stringify({ task: "resize", url: "https://..." }));

// Consumer — long-poll, process, then ack each receipt handle
const { messages } = await client.queue.receive(q.id, { max: 10, wait: 20 });
for (const m of messages) {
  // ...process m.body...
  await client.queue.ack(q.id, m.receipt_handle);
}
// → the SAME `client` also owns this user's email, cards, vault, and KYC.
The shape is largely the same — create, send, receive, delete — because Naive’s queue is Amazon SQS underneath. The real differences to plan for:
  • No key, no region wrangling. SQS needs IAM credentials and a region; the queue runs on your AWS trust. Naive runs the queue on cloud credentials it owns — the agent holds no cloud key and never sees a QueueUrl.
  • The queue is per-user, not per-account. SQS gives you one account; you hand-build tenant isolation with naming + IAM. On Naive naive.forUser(id).queue is the isolation boundary.
  • ReceiptHandlereceipt_handle, Bodybody. Field casing changes; the semantics (long-poll, visibility timeout, at-least-once, ack-to-delete) are the same SQS semantics.
  • The id is your identity, not a separate account. In SQS the key scopes SQS in one AWS account. In Naive the same forUser(id) handle also owns the agent’s email, cards, vault, and KYC.

FIFO, delay, and dead-letter queues

The other SQS features teams rely on — FIFO ordering, per-message delay, and a dead-letter queue — map directly, with less setup.
// DLQ first, then read its ARN, then wire a RedrivePolicy on the main queue
const { QueueUrl: dlqUrl } = await sqs.send(new CreateQueueCommand({
  QueueName: "orders-dlq.fifo",
  Attributes: { FifoQueue: "true" },
}));
const { Attributes } = await sqs.send(new GetQueueAttributesCommand({
  QueueUrl: dlqUrl, AttributeNames: ["QueueArn"],
}));

const { QueueUrl } = await sqs.send(new CreateQueueCommand({
  QueueName: "orders.fifo",
  Attributes: {
    FifoQueue: "true",
    RedrivePolicy: JSON.stringify({
      deadLetterTargetArn: Attributes!.QueueArn,
      maxReceiveCount: 5,
    }),
  },
}));

// FIFO send needs a MessageGroupId (and optionally a dedup id), plus optional delay
await sqs.send(new SendMessageCommand({
  QueueUrl,
  MessageBody: JSON.stringify({ order: 42 }),
  MessageGroupId: "u_123",
  MessageDeduplicationId: "order-42",
  DelaySeconds: 10,
}));
// One call: FIFO queue with a dead-letter queue provisioned for you
const q = await client.queue.create({ name: "orders", type: "fifo", dlq: true });

// FIFO send takes the group id (and optional dedup id), plus optional delay
await client.queue.send(q.id, JSON.stringify({ order: 42 }), {
  group_id: "u_123",
  dedup_id: "order-42",
  delay_seconds: 10,
});
  • DLQ is a flag, not a two-queue dance. SQS makes you create the DLQ, read its ARN, and attach a RedrivePolicy. Naive provisions the DLQ when you pass dlq: true and tears it down with the parent on destroy.
  • maxReceiveCount is not exposed on Naive — the redrive threshold is managed. If you tune it in SQS, note that as a gap.

Minimal viable migration

The smallest swap that keeps a working queue-backed agent alive is create → send → receive → ack.
1

Install the SDK and set your key

npm install @usenaive-sdk/server
Set NAIVE_API_KEY (a server-side key from the dashboard). You can drop AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY and the region config for this path.
2

Pick the identity that owns the queue

Use naive.queue for the account’s default agent profile, or naive.forUser(id).queue to scope the queue to a specific end-user. There is no QueueUrl and no region — Naive resolves them.
3

Swap queue creation

Replace CreateQueueCommand({ QueueName }) with naive.queue.create({ name }). For FIFO pass type: "fifo"; for a dead-letter queue pass dlq: true (no separate DLQ + RedrivePolicy). Keep the returned id in place of the QueueUrl.
4

Swap send

Map SendMessageCommand({ QueueUrl, MessageBody })naive.queue.send(id, body). Move MessageGroupId / MessageDeduplicationId / DelaySeconds to the { group_id, dedup_id, delay_seconds } options object.
5

Swap receive + ack

Map ReceiveMessageCommand({ MaxNumberOfMessages, WaitTimeSeconds })naive.queue.receive(id, { max, wait }), then read m.body / m.receipt_handle (lower-case). Map DeleteMessageCommand({ ReceiptHandle })naive.queue.ack(id, receipt_handle).
6

Ship it

At this point you are off SQS for the core create → send → receive → ack path. Everything below is upside, not a requirement.

Consolidate further once you’re on Naive

This is where the migration pays for itself. In SQS, the IAM credential scopes queues in one AWS account and nothing else, and you build per-tenant isolation with naming conventions and policies. On Naive, the unit of isolation is a tenant user, and the queue is one of many primitives that identity owns.
// One IAM credential scopes SQS — and only SQS — across your whole account.
// Per-tenant isolation is a naming + IAM policy you maintain yourself.
const { QueueUrl } = await sqs.send(new CreateQueueCommand({
  QueueName: `tenant-${customer.id}-jobs`, // your convention
}));
await sqs.send(new SendMessageCommand({ QueueUrl, MessageBody: "..." }));

// The card, the vault secrets, the email inbox, and the KYC for this customer's
// agent live in entirely separate systems with their own tenancy.
// One tenant user per customer isolates *every* primitive.
const acme = await naive.users.create({
  external_id: customer.id,
  email: customer.email,
});

const client = naive.forUser(acme.id);

// The queue is scoped to this user — no naming convention, no IAM policy:
const q = await client.queue.create({ name: "jobs" });
await client.queue.send(q.id, "...");

// The SAME client owns this customer's email inbox, card, vault, and KYC —
// one identity, isolated end to end.
await client.email.createInbox({ local_part: "agent" });
await client.cards.create({ spending_limit_cents: 25_000 });

Gain #1 — one identity across primitives

  • With SQS, the agent’s queues are a slice of one AWS account, and per-tenant isolation is a naming convention plus IAM policy you write and audit yourself. With Naive, naive.forUser(acme.id) is a single handle to queue and email and cards and vault and KYC.
  • Each queue is namespaced + tagged and locked to that tenant’s task role automatically — sibling tenants can never read each other’s queues, secrets, cards, or logs. Tear the customer down from one place.

Gain #2 — execution-time governance

  • Whether an agent may use the queue at all is policy on the identity — not an IAM policy you maintain in a second console. Toggle the queue primitive per user in the Account Kit, and every request is metered against the tenant’s plan quota.
// A starter tier: queue enabled, capped by the plan's quota; no card for this tier.
const starter = await naive.accountKits.create({
  name: "Starter",
  primitives_config: {
    queue: { enabled: true },
    cards: { enabled: false },
  },
});
await naive.accountKits.assignUser(starter.id, acme.id);

// The plan carries the per-primitive quota that meters queue usage at execution time.
await naive.plans.upsert({
  key: "starter",
  name: "Starter",
  accountKitId: starter.id,
  period: "month",
  quotas: { queue: 100_000 },
});
await naive.forUser(acme.id).billing.setSubscription({ planKey: "starter" });
  • The agent’s code path stays the same for every tier — client.queue.send(...), client.queue.receive(...). What changes at execution time:
    • Disabled primitive → the call is refused. If the Account Kit doesn’t grant queue, the request never reaches AWS.
    • Quota exceeded → 429. Once usage crosses the plan’s queue quota for the period, the call is rejected with a rate_limited error — no surprise AWS bill from a runaway agent.
    • The agent holds no cloud key. Naive owns the AWS credentials; a leaked agent key can’t enumerate or drain queues on your raw AWS account.
Unlike money-moving primitives (cards, trading), enqueuing a message is not approval-gated — there’s no human-in-the-loop pause on send. Queue governance is enable/disable + plan quota + unified logs, enforced at execution time. If you need per-action human approval, that lives on the sensitive primitives, not the queue.

Gain #3 — unified accountability

  • Every queue create, send, receive, and purge for a customer lands in one per-user activity log — alongside their email, card, vault, and KYC events, not in a separate CloudWatch namespace:
const { events } = await naive.forUser(acme.id).logs.query({ limit: 50 });
// "what did this agent enqueue, for whom?" — queue, email, cards, vault, one timeline
  • That is the question that is hard to answer when queues live in SQS/CloudWatch, secrets live in your own manager, cards live in Stripe, and email lives somewhere else. Under Naive it is a single query.

What does not map yet

A migration guide that hides gaps is worse than none. The core path (create → send → receive → ack), FIFO, per-message delay, dead-letter queues, purge, attributes, and long-polling map cleanly — the queue is Amazon SQS underneath. But the following SQS capabilities have no direct equivalent on Naive’s queue primitive today. Check this list against your app before you commit.
Amazon SQS featureStatus on NaiveWorkaround
MessageAttributes — typed metadata key/value map alongside the bodyNot providedEncode metadata inside the JSON body
Batch APIs (SendMessageBatch / DeleteMessageBatch, ≤10 per call)Not providedLoop single send / ack calls
ChangeMessageVisibility — extend a single in-flight message’s timeoutNot providedSet visibility at receive time; size it for your slowest processing
Redrive tuning (maxReceiveCount, RedrivePolicy, StartMessageMoveTask)dlq: true onlyDLQ is provisioned; the receive-count threshold is managed, not configurable
Queue attribute tuning (retention period, max message size, receive-wait default, KMS SSE)Managed defaultsNot tunable per queue today
Access policy / cross-account grants (AddPermission, SQS policy)Not applicableNaive owns the creds and scopes per tenant — that’s the point
Native SNS→SQS fan-out / EventBridge pipes / Lambda event-source mappingNot providedPoll the queue from a compute service worker
CloudWatch metrics / alarmsPer-user logs + attributes(id)Approximate depth via attributes; events via the activity log
Raw QueueUrl / ARN, region selectionQueue id onlyNaive resolves region/account; you never handle a URL or ARN
If your agent depends on SQS MessageAttributes, batch send/delete, per-message ChangeMessageVisibility, or fine-grained queue attribute / redrive tuning, those are the gaps most likely to need a workaround — Naive’s queue is a curated, per-tenant SQS surface (create / send / receive / ack / purge / attributes), not the full SQS control plane. The core producer/consumer path, FIFO, delay, and DLQ map directly. The flip side is the gain: the queue, its credentials, and its isolation are governed by the same identity and Account Kit as the rest of the agent — with per-plan quota and a unified audit trail — not a slice of an AWS account behind an all-or-nothing IAM key.

Where to go next

  • queue primitive — standard / FIFO, DLQ, send / receive / ack, worker pattern
  • queue SDK sub-client — typed method signatures
  • queue CLI — create / send / receive / ack / attributes
  • Compute — pair a service worker that long-polls the queue for a cheap autoscaling consumer
  • Customer billing — the plan + quota that meters queue usage at execution time
  • Account Kits — the policy model that enables/disables the queue per user
  • Tenant users — the identity that owns the queue, compute, email, cards, and vault