Publish an SNS Message With AWS SDK v3 in TypeScript

A tall radio antenna tower with several dishes against a clear blue sky

Photo by Kabiur Rahman Riyad on Unsplash

To publish an SNS message with AWS SDK v3, create an SNSClient in the topic’s Region and send a PublishCommand with TopicArn and a string Message. Add MessageAttributes so subscription filter policies can route it. FIFO topics also need MessageGroupId, and PublishBatchCommand sends up to 10 messages per call.

This guide is for Node.js and TypeScript developers who publish events to Amazon SNS: an order was created, a file was processed, a job failed. You’ll finish with a typed module that covers the SNS publish AWS SDK v3 calls you actually need in a service, with attributes that filter policies can match, FIFO publishing, batches that report partial failures, and the IAM policy to go with it.

Coming from v2’s sns.publish(params).promise()? The guide to migrate a Node.js app from AWS SDK v2 to v3 covers the client swap, and the free AWS SDK JavaScript v2 to v3 converter drafts it from your code.

Topic, phone number or endpoint: where can Publish send?

SNS is a publish-subscribe channel in the sense of Enterprise Integration Patterns’ Publish-Subscribe Channel: one message in, one copy out to every subscriber. Publish takes exactly one destination:

Parameter Sends to Use it for
TopicArn Every subscription on the topic (SQS, Lambda, HTTPS, email and more) Events that several consumers care about
PhoneNumber One SMS, E.164 format Direct text messages; SMS has its own account setup and spending controls
TargetArn One mobile platform endpoint Push notifications to one device

The rest of this guide publishes to topics. You can only publish to topics and endpoints in the same Region as your client. If consumers need to route on fields inside the event rather than on message attributes, compare it with the guide to send events to EventBridge with AWS SDK v3 (PutEvents). If one event has to drive several steps in order rather than fan out, start a Step Functions execution with AWS SDK v3 instead. If consumers need to read a high-volume, ordered stream and replay it, write records to Kinesis Data Streams with AWS SDK v3.

Prerequisites

How to use SNS publish in AWS SDK v3, step by step

  1. Create one SNSClient per processBuild it at module level, in the topic’s Region, so connections and credentials are reused.
  2. Serialize the bodyMessage is a UTF-8 string. Send JSON with JSON.stringify; subscribers parse it.
  3. Add attributes for routingMessageAttributes carry typed metadata (String, String.Array, Number, Binary) next to the body. Filter policies match on them by default.
  4. Add FIFO fields if the topic ends in .fifoMessageGroupId on every message, and MessageDeduplicationId unless the topic has content-based deduplication.
  5. Batch when you have manyPublishBatchCommand takes up to 10 entries, and you must check Failed even on success.
  6. Keep the MessageIdLog it with your own event ID; it’s what you search for when a subscriber says a message never arrived.

Example: an order-events publisher

sns-publish.ts

// sns-publish.ts
// Publishing to Amazon SNS with AWS SDK for JavaScript v3: attributes for filtering, FIFO topics and batches.
import { randomUUID } from "node:crypto";
import {
  SNSClient,
  PublishBatchCommand,
  PublishCommand,
  type MessageAttributeValue,
  type PublishBatchRequestEntry,
} from "@aws-sdk/client-sns";

export const sns = new SNSClient({}); // region from AWS_REGION or your profile; must match the topic's Region

export interface OrderEvent {
  orderId: string;
  customerId: string;
  status: "CREATED" | "SHIPPED" | "CANCELLED";
  total: number;
}

// Attributes travel next to the body. Subscription filter policies match on them by default.
function attributesFor(e: OrderEvent): Record<string, MessageAttributeValue> {
  return {
    eventType: { DataType: "String", StringValue: `order.${e.status.toLowerCase()}` },
    total: { DataType: "Number", StringValue: String(e.total) },
  };
}

/** 1. Publish one event to a standard topic. Returns the MessageId. */
export async function publishOrderEvent(topicArn: string, e: OrderEvent): Promise<string> {
  const out = await sns.send(new PublishCommand({
    TopicArn: topicArn,
    Message: JSON.stringify(e),
    MessageAttributes: attributesFor(e),
  }));
  return out.MessageId ?? "";
}

/** 2. Publish to a FIFO topic (name ends in .fifo): a group ID is required, and a dedup ID
 *     unless the topic has content-based deduplication. */
export async function publishOrderEventFifo(topicArn: string, e: OrderEvent): Promise<{ messageId: string; sequence: string }> {
  const out = await sns.send(new PublishCommand({
    TopicArn: topicArn,
    Message: JSON.stringify(e),
    MessageAttributes: attributesFor(e),
    MessageGroupId: e.orderId, // ordered per order, parallel across orders
    MessageDeduplicationId: `${e.orderId}-${e.status}`, // a retry of the same change is dropped for 5 minutes
  }));
  return { messageId: out.MessageId ?? "", sequence: out.SequenceNumber ?? "" };
}

/** 3. Publish many events, 10 per PublishBatch call. Returns the events that failed. */
export async function publishMany(topicArn: string, events: OrderEvent[]): Promise<{ event: OrderEvent; code: string; message: string }[]> {
  const failed: { event: OrderEvent; code: string; message: string }[] = [];
  for (let i = 0; i < events.length; i += 10) {
    const chunk = events.slice(i, i + 10);
    const byId = new Map<string, OrderEvent>();
    const entries: PublishBatchRequestEntry[] = chunk.map((e) => {
      const Id = randomUUID(); // must be unique within the batch
      byId.set(Id, e);
      return { Id, Message: JSON.stringify(e), MessageAttributes: attributesFor(e) };
    });
    const out = await sns.send(new PublishBatchCommand({ TopicArn: topicArn, PublishBatchRequestEntries: entries }));
    // HTTP 200 can still contain failed entries: always check Failed.
    for (const f of out.Failed ?? []) {
      const event = byId.get(f.Id ?? "");
      if (event) failed.push({ event, code: f.Code ?? "Unknown", message: f.Message ?? "" });
    }
  }
  return failed;
}

Calling it from a script:

run-publish.ts

// run-publish.ts
// Usage: TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:orders npx tsx run-publish.ts
import { publishMany, publishOrderEvent, type OrderEvent } from "./sns-publish.js";

const topicArn = process.env.TOPIC_ARN;
if (!topicArn) {
  console.error("Set TOPIC_ARN to the topic you want to publish to.");
  process.exit(1);
}

const first: OrderEvent = { orderId: "o-1001", customerId: "c-42", status: "CREATED", total: 129.5 };
console.log("published", await publishOrderEvent(topicArn, first));

const backlog: OrderEvent[] = Array.from({ length: 23 }, (_, n) => ({
  orderId: `o-${2000 + n}`,
  customerId: "c-42",
  status: n % 5 === 0 ? "CANCELLED" : "SHIPPED",
  total: 20 + n,
}));
const failed = await publishMany(topicArn, backlog);
console.log(`batch: ${backlog.length - failed.length} published, ${failed.length} failed`);
for (const f of failed) console.log(f.event.orderId, f.code, f.message);
Terminal

npm install @aws-sdk/client-sns
npm install --save-dev tsx typescript

AWS_PROFILE=dev AWS_REGION=us-east-1 TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:orders npx tsx run-publish.ts
# published 5b7f3c1e-8a2d-5f4b-9c61-0e3d2a7b8c90
# batch: 23 published, 0 failed

What to notice in the module:

  • Attributes are strings on the wire. Even a Number attribute is sent as StringValue; the DataType tells SNS to validate and compare it as a number. Name, type and value all count toward the message size limit.
  • The FIFO dedup ID is a business key. orderId-status means a retried publish of the same change is accepted but not delivered again within the 5-minute deduplication interval, while the next status change still goes through.
  • Batch IDs are only for matching results. They must be unique within one request. The module keeps a map from ID to event so failed entries can be retried or logged.

How do message attributes and filter policies work together?

Without a filter policy, every subscriber gets every message. With one, SNS compares the policy to the message’s attributes (the default scope, MessageAttributes) or to its JSON body (scope MessageBody) and delivers only matches. This policy, set as the FilterPolicy attribute on a refunds queue’s subscription, accepts only cancelled orders of 100 or more:

filter-policy.json

{
  "eventType": ["order.cancelled"],
  "total": [{ "numeric": [">=", 100] }]
}

Two details catch people out. Message attributes are sent only when the message structure is a plain string, not with MessageStructure: "json". And for SQS subscriptions with raw message delivery turned on, at most 10 message attributes are delivered; messages with more are discarded as client-side errors.

For the queue itself to accept messages from the topic, its access policy must allow sqs:SendMessage for the SNS service principal, limited to your topic with aws:SourceArn:

sqs-queue-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "sns.amazonaws.com" },
      "Action": "sqs:SendMessage",
      "Resource": "arn:aws:sqs:us-east-1:123456789012:order-refunds",
      "Condition": {
        "ArnEquals": { "aws:SourceArn": "arn:aws:sns:us-east-1:123456789012:orders" }
      }
    }
  ]
}

Without the aws:SourceArn condition, any topic could send to the queue; the script to find public SNS topics and SQS queues flags policies like that. From there, the consumer is ordinary SQS code; the guide to send and receive SQS messages with AWS SDK v3 in TypeScript covers receiving and deleting. Give that queue a redrive policy too; the script to find SQS queues without a dead-letter queue checks which subscribed queues lack one. Without raw message delivery, the SQS body is an SNS envelope JSON whose Message field holds your string.

Which IAM permissions does publishing need?

sns:Publish on the topic ARN covers both Publish and PublishBatch. If the topic uses server-side encryption with a KMS key, the publisher also needs kms:GenerateDataKey* and kms:Decrypt on that key; drop the second statement for unencrypted topics.

sns-publish-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublishOrderEvents",
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": [
        "arn:aws:sns:us-east-1:123456789012:orders",
        "arn:aws:sns:us-east-1:123456789012:orders.fifo"
      ]
    },
    {
      "Sid": "EncryptedTopicOnly",
      "Effect": "Allow",
      "Action": ["kms:GenerateDataKey*", "kms:Decrypt"],
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
    }
  ]
}

To derive the actions from your own code, see how to find the IAM actions your AWS SDK for JavaScript code needs, or paste the module into the IAM policy generator for TypeScript code.

Troubleshooting common SNS publish errors

  • AuthorizationError. The caller lacks sns:Publish on this topic, or a topic policy denies it. The steps to troubleshoot AWS IAM access denied errors apply here too.
  • NotFound. Wrong topic ARN or wrong Region. Log await sns.config.region() next to the ARN.
  • InvalidParameter on a FIFO topic. Missing MessageGroupId, or no MessageDeduplicationId on a topic without content-based deduplication. The same error covers a message plus attributes larger than the topic’s size limit.
  • KMSAccessDenied or KMSDisabled. The topic is encrypted and the publisher can’t use the key, or the key is disabled.
  • TooManyEntriesInBatchRequest, BatchEntryIdsNotDistinct. More than 10 entries, or a repeated Id, in one PublishBatch.
  • Publish succeeds, subscriber gets nothing. A filter policy doesn’t match, the SQS queue policy doesn’t allow the topic, or the subscription is still pending confirmation.
  • Throttling under load. The SDK retries throttling errors automatically; to tune attempts and backoff, see how to configure retry and timeout settings in AWS SDK for JavaScript v3.

Limits and costs

  • Size. By default a message can be up to 256 KiB (262,144 bytes), body and attributes together. A topic’s MaximumMessageSize attribute can raise that to 1 MiB, and for PublishBatch the limit applies to the whole batch.
  • Batch size. 10 messages per PublishBatch call.
  • Subject. Used for email subscriptions; under 100 characters, no line breaks. Email subscriptions suit notifications; for formatted mail to customers, send email with Amazon SES and AWS SDK v3 in TypeScript instead.
  • Price. As of September 2026, the AWS Price List gives $0.50 per million SNS API requests in us-east-1 after the first million each month, and no charge for deliveries to SQS. Other delivery types, and FIFO topics, are priced separately.

To see what’s subscribed to a topic before you change the message format, ChatWithCloud can answer “Which subscriptions does the orders topic have, and do any have filter policies?” from a read-only profile; it runs the AWS calls on your machine and sends the results to the AI model to write the answer. If you’re porting a Python publisher, the boto3 to AWS SDK v3 converter drafts the TypeScript version.

Frequently asked questions

How do I publish a JSON message to SNS in Node.js?

Send new PublishCommand({ TopicArn, Message: JSON.stringify(payload) }) through an SNSClient. Subscribers receive the string and parse it.

What’s the difference between Publish and PublishBatch?

Publish sends one message and throws on failure. PublishBatch sends up to 10 to one topic and returns Successful and Failed lists, so a 200 response can still contain failures.

Do I need MessageGroupId for a standard SNS topic?

No. It’s required on FIFO topics. On standard topics it’s optional and only forwarded to SQS standard subscriptions for fair queues.

How do I send an SNS message to only some subscribers?

Add message attributes when you publish and set a filter policy on each subscription that should receive only a subset.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud