DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

How to Combine Claude’s Function Calling with SNS FIFO for Reliable, Ordered AI Notifications

LLMs can now call tools, but turning their output into a trustworthy event stream is still a puzzle. We wire Claude’s function‑calling to an SNS FIFO topic, giving you ordered, deduplicated notifications that downstream Lambda functions can consume with zero‑loss guarantees.


Why SNS FIFO Is a Good Fit for LLM‑Generated Events

When an LLM decides to “publishAlert”, you usually want the alert to be processed exactly in the order it was generated. Imagine a fire‑alarm system that first warns about a smoke detector, then follows up with a sprinkler‑activation command. If those two messages arrive swapped, you could end up turning on sprinklers before the fire is even confirmed.

FIFO stands for First‑In‑First‑Out. An SNS FIFO topic guarantees that messages sharing the same MessageGroupId are delivered to subscribers in the exact order they were published. This is different from the default “standard” SNS topics, which deliver messages quickly but without ordering guarantees.

In plain English: SNS FIFO is like a single‑lane road with a traffic light that lets cars (messages) pass one after another, never overtaking.

Key terms (first use)

Term Meaning
Function calling A feature where the LLM can invoke a pre‑defined tool (a piece of code) instead of just returning text.
FIFO topic An SNS topic that preserves the order of messages that belong to the same logical group.
MessageGroupId An identifier that tells SNS which messages belong together for ordering.
MessageDeduplicationId A token that prevents the same message from being delivered twice within a 5‑minute window.
Lambda A serverless compute service that runs code in response to events (like an SNS message).

Because the LLM can generate many alerts rapidly, using a FIFO topic means you can treat the AI as a deterministic producer rather than a chaotic chatterbox. The downstream Lambda sees the alerts in the same sequence the model emitted them.


Setting Up Claude’s Function Calls to Publish to SNS

Before you can send anything to SNS, Claude (the LLM) needs to know about the tool you’re exposing. In Claude’s terminology a tool schema describes the name, description, and the JSON shape of the arguments it can pass.

Below is a minimal TypeScript snippet that creates a tool called publishAlert. The function body uses the AWS SDK v3 (@aws-sdk/client-sns) to push a message onto the FIFO topic. Notice the use of the satisfies keyword – it tells TypeScript “this object matches the shape I described, but don’t widen the type”.

// src/claudeTool.ts
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

// ---------------------------------------------------------------------
// 1️⃣  Prepare the SNS client – it will read credentials from the
//    environment (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.).
// ---------------------------------------------------------------------
const snsClient = new SNSClient({ region: "us-east-1" });

// ---------------------------------------------------------------------
// 2️⃣  Define the shape of the arguments Claude is allowed to send.
//    This is the contract between the LLM and our code.
// ---------------------------------------------------------------------
type PublishAlertArgs = {
  /** Human‑readable title of the alert */
  title: string;
  /** Optional JSON payload that downstream systems care about */
  payload: Record<string, unknown>;
  /** Group ID to keep ordering – e.g., a device ID or tenant ID */
  groupId: string;
};

// ---------------------------------------------------------------------
// 3️⃣  The tool schema Claude will load.  The `satisfies` keyword forces
//    the object to be exactly the type we described above.
// ---------------------------------------------------------------------
export const publishAlertTool = {
  name: "publishAlert",
  description: "Publish an ordered alert to an SNS FIFO topic",
  input_schema: {
    type: "object",
    properties: {
      title: { type: "string" },
      payload: { type: "object" },
      groupId: { type: "string" },
    },
    required: ["title", "groupId"],
    additionalProperties: false,
  },
} satisfies { name: string; description: string; input_schema: object };

// ---------------------------------------------------------------------
// 4️⃣  The implementation that Claude will invoke.  It builds the SNS
//    PublishCommand with the required FIFO fields.
// ---------------------------------------------------------------------
export async function publishAlert(args: PublishAlertArgs): Promise<void> {
  const { title, payload, groupId } = args;

  // A stable deduplication ID – you could hash the payload, add a timestamp,
  // or use a UUID if you need absolute uniqueness.
  const dedupId = `${groupId}-${Date.now()}`;

  const command = new PublishCommand({
    // The ARN of the FIFO topic you created (ends with .fifo)
    TopicArn: process.env.ALERTS_FIFO_TOPIC_ARN,
    // Message body – keep it short; you can embed a JSON string if needed.
    Message: JSON.stringify({ title, payload }),
    // Guarantees ordering for all alerts that share this groupId.
    MessageGroupId: groupId,
    // Prevents the same alert from being sent twice within 5 minutes.
    MessageDeduplicationId: dedupId,
  });

  // Send the command; any error will bubble up to Claude as a tool failure.
  await snsClient.send(command);
}
Enter fullscreen mode Exit fullscreen mode

Tip: Keep the MessageDeduplicationId deterministic (e.g., a hash of the payload) if you ever need exactly‑once semantics across retries.

The LLM will call publishAlert whenever it decides an alert should be raised. Your application simply needs to expose the publishAlertTool description to Claude and bind the publishAlert implementation to the tool handler.


Configuring an SNS FIFO Topic with Message Grouping and Deduplication

Creating a FIFO topic is a one‑time operation, but there are a few hidden rules that bite many engineers:

  1. FIFO topics require a matching FIFO subscription – you cannot subscribe a standard SQS queue or an HTTP endpoint that is not FIFO‑aware.
  2. Message attributes are limited to five per subscription – try to keep metadata minimal.
  3. Delivery retries happen per subscriber. If a Lambda invocation fails, SNS will retry up to three times, then give up silently unless you monitor the CloudWatch metrics.

Below is a small script that creates a FIFO topic, sets the required attributes, and adds a Lambda subscription. The code uses the same SDK (@aws-sdk/client-sns) and demonstrates the gotchas.

// scripts/createFifoTopic.ts
import {
  SNSClient,
  CreateTopicCommand,
  SubscribeCommand,
  SetTopicAttributesCommand,
} from "@aws-sdk/client-sns";

// ---------------------------------------------------------------------
// 1️⃣  Initialize the client (same region as your Lambda)
// ---------------------------------------------------------------------
const sns = new SNSClient({ region: "us-east-1" });

async function main() {
  // -----------------------------------------------------------------
  // 2️⃣  Create the FIFO topic.  The name MUST end with ".fifo".
  // -----------------------------------------------------------------
  const createResp = await sns.send(
    new CreateTopicCommand({
      Name: "ai-alerts.fifo",
      Attributes: {
        // FIFO topics need these two flags.
        FifoTopic: "true",
        // Optional: set a default message group to avoid errors if you forget.
        // We'll enforce explicit group IDs later.
        ContentBasedDeduplication: "false",
      },
    })
  );

  const topicArn = createResp.TopicArn!;
  console.log("✅ FIFO topic created:", topicArn);

  // -----------------------------------------------------------------
  // 3️⃣  Attach a Lambda subscriber (replace with your function ARN).
  // -----------------------------------------------------------------
  const lambdaArn = process.env.ALERTS_LAMBDA_ARN!;
  await sns.send(
    new SubscribeCommand({
      Protocol: "lambda",
      TopicArn: topicArn,
      Endpoint: lambdaArn,
    })
  );
  console.log("✅ Lambda subscribed:", lambdaArn);

  // -----------------------------------------------------------------
  // 4️⃣  (Optional) Add a dead‑letter queue (DLQ) via a subscription
  //     attribute – note that SNS FIFO does NOT create a DLQ automatically.
  // -----------------------------------------------------------------
  await sns.send(
    new SetTopicAttributesCommand({
      TopicArn: topicArn,
      AttributeName: "RedrivePolicy",
      AttributeValue: JSON.stringify({
        deadLetterTargetArn: process.env.ALERTS_DLQ_ARN,
      }),
    })
  );
  console.log("✅ DLQ attached (if provided).");
}

main().catch((err) => {
  console.error("❌ Error creating topic:", err);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Key takeaway: A FIFO topic is only as reliable as its subscribers. Make sure the Lambda you attach is ready to handle retries, and consider wiring a dead‑letter queue manually because SNS does not add one by default.

Gotcha deep‑dive

  • Deduplication window – SNS remembers each MessageDeduplicationId for 5 minutes. If you reuse the same ID within that window, the second message disappears without any error. To avoid silent drops, generate a fresh ID for each publish (as shown) or enable ContentBasedDeduplication and let SNS hash the Message body.

  • Ordering across groups – SNS only guarantees order inside a single MessageGroupId. If you publish alerts for two different devices (groupId = "deviceA" and "deviceB"), their relative order is undefined. Design your downstream logic to treat each group independently, or funnel everything through a single group if true global order is required (at the cost of throughput).


Consuming Ordered Events with a Lambda Subscriber

Now that alerts are flowing into SNS, we need a Lambda that respects the ordering and logs the payload. The Lambda runtime we’ll target is Node.js 22, the latest LTS version. Be aware of two Lambda‑specific gotchas:

  • require(esm) in Node 22 can break existing Lambda layers silently – always use native ESM (import …) or stay with CommonJS.
  • Provisioned Concurrency (pre‑warming) costs money even when idle – monitor usage before enabling it.

Below is a straightforward handler that extracts the SNS message, parses the JSON payload, and logs the alert. It also explicitly acknowledges the message by returning successfully; any uncaught error will cause SNS to retry the delivery.

// src/alertProcessor.ts
import { SQSEvent, SNSEvent, Context } from "aws-lambda";

/**
 * Lambda entry point – SNS will invoke this function for each batch
 * of messages that share the same MessageGroupId.
 */
export async function handler(event: SNSEvent, _ctx: Context): Promise<void> {
  // SNS may deliver multiple records in one invocation.
  for (const record of event.Records) {
    // -----------------------------------------------------------------
    // 1️⃣  The raw message body is a string; we expect JSON.
    // -----------------------------------------------------------------
    const raw = record.Sns.Message;
    let parsed: { title: string; payload?: Record<string, unknown> };

    try {
      parsed = JSON.parse(raw);
    } catch (e) {
      // If parsing fails, we *must* let the error bubble up so SNS retries.
      console.error("❌ Failed to parse SNS message:", raw);
      throw e;
    }

    // -----------------------------------------------------------------
    // 2️⃣  Log the alert – in a real system you would forward it to a DB
    //     or another service.
    // -----------------------------------------------------------------
    console.log(
      `🔔 Alert [${record.Sns.MessageGroupId}]: ${parsed.title}`,
      parsed.payload ?? {}
    );
  }

  // Returning without error tells SNS the batch was processed.
}
Enter fullscreen mode Exit fullscreen mode

To wire this function to the SNS topic, you can use the AWS Console or the CDK/CloudFormation. The critical configuration bits are:

Setting Value Why it matters
Runtime nodejs22.x Supports the latest language features and the SDK v3.
Memory 128 MiB (or higher if payloads are large) Affects max concurrent invocations; keep low for cost.
Timeout 30 seconds (default) Should be enough for simple logging; increase if you do heavy work.
Dead‑letter queue Optional, but recommended SNS retries three times; after that the message is lost unless a DLQ captures it.

Tip: Enable CloudWatch Logs for the Lambda and set an alarm on InvocationErrors. Because SNS retries are per‑subscriber, a silent Lambda failure could leave you with undelivered alerts.


Testing and Debugging the End‑to‑End Flow

A reliable system is only as good as the tests you run against it. The following steps let you validate ordering, deduplication, and error handling without deploying to production.

1️⃣ Local “Claude” simulation

Create a tiny script that calls publishAlert a few times with the same groupId. Use a short setTimeout between calls to mimic rapid LLM output.

// scripts/simulateClaude.ts
import { publishAlert } from "../src/claudeTool";

async function main() {
  const groupId = "device-123";

  // Fire three alerts in quick succession.
  await publishAlert({
    title: "Temperature high",
    payload: { temp: 78 },
    groupId,
  });
  await publishAlert({
    title: "Temperature critical",
    payload: { temp: 92 },
    groupId,
  });
  await publishAlert({
    title: "Shutdown initiated",
    payload: { reason: "overheat" },
    groupId,
  });

  console.log("✅ All alerts sent.");
}

main().catch((e) => {
  console.error("❌ Simulation failed:", e);
});
Enter fullscreen mode Exit fullscreen mode

Run ts-node scripts/simulateClaude.ts. Then check the Lambda logs – you should see the three alerts appear in the same order.

2️⃣ Verify deduplication

Modify the script to reuse the same MessageDeduplicationId (by passing a constant dedupId into publishAlert). You’ll see only the first message appear in Lambda logs; the others are dropped silently. This demonstrates the 5‑minute window rule.

3️⃣ Force a Lambda error

Add a line that throws an exception for a particular alert (e.g., when title contains “critical”). Deploy the Lambda, run the simulation again, and watch CloudWatch. You’ll see the failed invocation retried three times, then disappear unless you have a DLQ attached.

In plain English: If the Lambda crashes, SNS will try three more times, then give up. Without a dead‑letter queue, that alert is lost forever.

4️⃣ Use CloudWatch Insights to confirm ordering

Run a query like the following in CloudWatch Logs Insights:

fields @timestamp, @message
| filter @message like /Alert/
| sort @timestamp asc
| limit 20
Enter fullscreen mode Exit fullscreen mode

The sort asc will show you the exact arrival order. If you see out‑of‑order entries for the same groupId, double‑check that you used a FIFO topic and that the MessageGroupId is identical across the batch.


The Takeaway

What you now have: a pattern that turns Claude’s tool calls into a reliable, ordered event stream using only AWS‑managed services.

  • FIFO topics keep per‑group order – every alert that shares a MessageGroupId arrives at the subscriber in the exact sequence it was published.
  • Deduplication IDs protect against accidental repeats but must be unique for 5 minutes; otherwise SNS silently discards later messages.
  • SNS retries are per‑subscriber, so monitoring Lambda errors and optionally adding a dead‑letter queue is essential.
  • Lambda’s simple handler can safely log or forward alerts; just make sure you return without error to acknowledge the message.
  • Testing locally (simulating Claude, forcing errors, inspecting CloudWatch) catches ordering and deduplication bugs before they reach production.

With these pieces in place, you can let Claude act as the brain of your system while SNS FIFO and Lambda act as the nervous system that reliably carries the signals in order, without loss. Happy coding!


Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-08-26 · Primary focus: SNS

All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.

Find an error? Drop a comment — corrections are always welcome.

Top comments (0)