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
- Node.js 18 or later, TypeScript and
tsx. The module usesrandomUUID()from Node’s built-innode:cryptomodule for batch entry IDs. @aws-sdk/client-sns.- An existing topic and its ARN. If the ARN comes from configuration, the guide to get SSM Parameter Store values with AWS SDK v3 shows a cached loader.
- Credentials the SDK can resolve; the guide to the AWS SDK v3 credentials provider chain explains the order it checks.
How to use SNS publish in AWS SDK v3, step by step
- Create one SNSClient per processBuild it at module level, in the topic’s Region, so connections and credentials are reused.
- Serialize the body
Messageis a UTF-8 string. Send JSON withJSON.stringify; subscribers parse it. - Add attributes for routing
MessageAttributescarry typed metadata (String,String.Array,Number,Binary) next to the body. Filter policies match on them by default. - Add FIFO fields if the topic ends in .fifo
MessageGroupIdon every message, andMessageDeduplicationIdunless the topic has content-based deduplication. - Batch when you have many
PublishBatchCommandtakes up to 10 entries, and you must checkFailedeven on success. - 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
// 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
// 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);
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
Numberattribute is sent asStringValue; theDataTypetells 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-statusmeans 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:
{
"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:
{
"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.
{
"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 lackssns:Publishon 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. Logawait sns.config.region()next to the ARN.InvalidParameteron a FIFO topic. MissingMessageGroupId, or noMessageDeduplicationIdon a topic without content-based deduplication. The same error covers a message plus attributes larger than the topic’s size limit.KMSAccessDeniedorKMSDisabled. 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 repeatedId, in onePublishBatch.- 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
MaximumMessageSizeattribute can raise that to 1 MiB, and forPublishBatchthe limit applies to the whole batch. - Batch size. 10 messages per
PublishBatchcall. - 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