header image
Back to Blog
awsslacktelegramserverlesseda

Why Telegram might be better than Slack for Serverless Business Notifications on AWS

In this article, we’re going to walk through why we replaced our Slack notification pipeline with the Telegram Bot API for real-time business alerts in our startup, how we built it using EventBridge, SQS, Lambda, and CDK, and why Telegram might be the better choice for your serverless notification needs based on cost and integration ease.

In this article, we’re going to walk through why we replaced our Slack notification pipeline with the Telegram Bot API for real-time business alerts in our startup, how we built it using EventBridge, SQS, Lambda, and CDK, and why Telegram might be the better choice for your serverless notification needs based on cost and integration ease.

This is relevant for any small team running a serverless platform that needs instant visibility into business (domain) events: signups, purchases, support tickets, and pipeline deployments, all without the overhead and cost of Slack’s email-to-channel pipeline or Webhooks, or these messages getting mixed up with team channels and chat messaging.

“The goal is simple: the moment something important happens on your platform that you want to know about in realtime, you get a formatted, actionable notification in your pocket; without paying for Slack, without SES email routing, and without the latency of email-to-channel delivery.”

Firstly, what is Telegram?

"Telegram is a messaging app with a focus on speed and security, it’s super-fast, simple and free. You can use Telegram on all your devices at the same time — your messages sync seamlessly across any number of your phones, tablets or computers. Telegram is one of the top 5 most downloaded apps in the world with over 1 billion active users.

With Telegram, you can send messages, photos, videos and files of any type (doc, zip, mp3, etc), as well as create groups for up to 200,000 people or channels for broadcasting to unlimited audiences. You can write to your phone contacts and find people by their usernames. As a result, Telegram is like SMS and email combined — and can take care of all your personal or business messaging needs. We also support end-to-end encrypted voice and video calls, group calls for up to 200 participants, and voice chats in groups that members can join whenever they want."

Why not just use Webhooks on Slack?

We found that there were some limitations with this integration on Slack when in the free tier, namely 1 webhook per second max, only 90 days of message history stored (Telegram is unlimited), and only ten integrations at most on Slack (we have distinct channels per notification type - and it is unlimited with Telegram).


The Problem We’re Solving 🎯

When you’re running a SaaS platform, and especially when you are an early startup, you need real-time visibility into what’s happening. Not CloudWatch dashboards you check once a day, but instant push notifications that hit your phone or tablet the moment a customer signs up, makes a purchase, or submits a support ticket.

Most teams reach for Slack automatically. It’s the default, I get it! But here’s what we discovered after running a Slack notification pipeline for 4 months:

  • Cost: Slack’s free tier limits message history and only allows email-to-channel message delivery or webhooks for the first few months for free. Pro plans start at about $8.75/user/month (which soon racks up for a small startup).
  • Latency: Email-to-Slack delivery adds 5-30 seconds of latency on top of your pipeline (even though it's the simplest integration path).
  • Mobile experience: the formatting of the email to channel message is poor (really, it looks like an ill-formatted email!). Webhooks are possible, too, of course, with extra configuration.

We wanted something simpler, faster, and ultimately, free.

Why Telegram? 📱

Telegram’s Bot API is purpose-built for exactly this use case. Here’s why it won out for us personally:

Aspect

Slack (via SES)

Telegram Bot API

Cost

SES charges + Slack Pro license

Completely free

Latency

5-30s (email routing) or < 1s (direct HTTPS webhook)

< 1s (direct HTTPS POST)

Setup

SES domain verification, Slack channel email config, etc

Create bot, get token, done

Rate limit

None

30 messages/sec per bot

Mobile

Heavy Slack app

Lightweight Telegram app, and separate to team chats

Formatting

Plain text email body

Markdown with emoji, bold, monospace

Dependencies

AWS SES SDK, email templates

Single fetch call (can work in our solution and GitHub Actions pipeline easily)

Infrastructure

SES permissions, domain DNS, bounce handling, etc

One SSM parameter (bot token)

Security

Excellent end-to-end encryption

Excellent end-to-end encryption

The killer feature? A single HTTPS POST with fetch - and it is free. To get the same features with Slack using Webhooks was going to cost us around $20 a month.

Our Example

We’re building this for an online learning platform where we need notifications for the following (which we can turn on and off per domain event):

  • New user signups — know when someone joins.
  • Course purchases — celebrate revenue in real-time.
  • Support tickets — respond quickly to customer issues.
  • Video uploads — track content creator activity.
  • CloudWatch alarms — infrastructure alerts in the same place.
  • Deployment status — CI/CD pipeline results from our GitHub Actions pipeline.

💡 Note: All code examples are for discussion only and can be further productionised.

Architecture Overview 🏗️

The notification system has three paths, each optimised for its source:

┌──────────────────────────────────────────────────────────────────────────┐
│                        Domain Event Path                                 │
│                                                                          │
│  ┌─────────────┐     ┌──────────────┐     ┌──────────────────────────┐   │
│  │ EventBridge │────▶│  SQS Queue   │────▶│  Notification Processor  │   │
│  │ (domain     │     │  (buffering, │     │  Lambda                  │   │
│  │  events)    │     │   DLQ, retry)│     │  - Validates (Zod)       │   │
│  └─────────────┘     └──────────────┘     │  - Formats (MarkdownV2)  │   │
│                                           │  - Sends (Bot API)       │   │
│                                           └──────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│                        Alarm Path                                        │
│                                                                          │
│  ┌─────────────┐     ┌───────────────────────────┐                       │
│  │ SNS Topic   │────▶│  Alarm Forwarder Lambda   │                       │
│  │ (CloudWatch │     │  - Parses alarm structure │                       │
│  │  alarms)    │     │  - Formats (MarkdownV2)   │                       │
│  └─────────────┘     │  - Sends (Bot API)        │                       │
│                      └───────────────────────────┘                       │
└──────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│                        Pipeline Path                                     │
│                                                                          │
│  ┌─────────────┐     ┌───────────────────────────┐                       │
│  │ GitHub      │────▶│  curl --data-urlencode    │                       │
│  │ Actions     │     │  (direct Bot API call)    │                       │
│  └─────────────┘     └───────────────────────────┘                       │
└──────────────────────────────────────────────────────────────────────────┘

                              │
                              ▼
                   ┌──────────────────────┐
                   │  Telegram Bot API    │
                   │  api.telegram.org    │
                   │                      │
                   │  ┌────────────────┐  │
                   │  │ 📱 Signups     │  │
                   │  │ 💰 Purchases   │  │
                   │  │ 🎫 Support     │  │
                   │  │ 🎬 Uploads     │  │
                   │  │ 🚨 Alarms      │  │
                   │  │ 🚀 Deploys     │  │
                   │  └────────────────┘  │
                   └──────────────────────┘

Three paths, one destination. Each is optimised for its trigger source, all converging on the same Telegram Bot API through a secondary adapter.

The Telegram Adapter: Replacing an Entire SES Pipeline with 30 Lines

Here’s the core of the solution; a secondary adapter that replaces the entire SES email sending pipeline:

tsx
export interface SendTelegramMessageParams {
  botToken: string;
  chatId: string;
  text: string;
  parseMode: 'HTML' | 'Markdown' | 'MarkdownV2';
}

export interface SendTelegramMessageResult {
  success: boolean;
  messageId?: number;
  errorMessage?: string;
}

export async function sendTelegramMessage(
  params: SendTelegramMessageParams,
): Promise<SendTelegramMessageResult> {
  const { botToken, chatId, text, parseMode } = params;

  const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
  const body = new URLSearchParams({
    chat_id: chatId,
    text,
    parse_mode: parseMode,
  });

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
      signal: controller.signal,
    });

    clearTimeout(timeoutId);
    const data = await response.json();

    if (!response.ok) {
      return {
        success: false,
        errorMessage: data?.description ?? `HTTP${response.status} error`,
      };
    }

    return { success: true, messageId: data?.result?.message_id };
  } catch (error) {
    clearTimeout(timeoutId);

    if (error instanceof Error && error.name === 'AbortError') {
      return { success: false, errorMessage: 'Request timed out after 5 seconds' };
    }

    return {
      success: false,
      errorMessage: error instanceof Error ? error.message : 'Unknown network error',
    };
  }
}

Compare this to what we had before: an SES adapter that needed AWS SDK imports, IAM permissions for ses:SendEmail, domain verification, bounce handling configuration, and email template formatting. The Telegram adapter is a single fetch POST call with a 5-second timeout.

Key design decisions:

  • Native fetch — no SDK dependencies, available in Node.js 18+ Lambda runtimes.
  • 5-second timeout — prevents Lambda from hanging if Telegram is slow.
  • Structured result — consistent interface for success/failure handling.
  • Bot token never exposed — sanitised from all error messages and logs.
  • Plug and play - we can use this stack for any Telegram notifications (pipeline, domain events, other...).

The Notification Processor Lambda: SQS Batch Processing

The main Lambda processes domain events from SQS with partial batch failure reporting; if one message in a batch of 10 fails, only that message is retried:

tsx
import { BatchProcessor, EventType, processPartialResponse } from '@aws-lambda-powertools/batch';
import { getParameter } from '@aws-lambda-powertools/parameters/ssm';
import { config } from '@config';

const processor = new BatchProcessor(EventType.SQS);

const botTokenSsmPath = config.get('telegramBotTokenSsmPath') as string;
const chatIdSignups = config.get('telegramChatIdSignups') as string;
const chatIdPurchases = config.get('telegramChatIdPurchases') as string;
// more where needed...

const eventTypeToChatId: Record<string, string | undefined> = {
  UserSignedUp: chatIdSignups,
  CoursePaymentMade: chatIdPurchases,
  SupportTicketCreated: chatIdSupportTickets,
  VideoUploadCompleted: chatIdFileUploads,
};

async function recordHandler(record: SQSRecord): Promise<void> {
  const event = parseEventBridgeEvent(record);
  if (!event) {
    metrics.addMetric('InvalidMessageFormat', MetricUnit.Count, 1);
    return; // Skip — don't retry invalid messages
  }

  const { detailType, detail } = event;
  const chatId = eventTypeToChatId[detailType];

  if (!chatId) {
    throw new Error(`Missing chat ID mapping for:${detailType}`);
  }

  const message = formatNotification(detailType, detail);
  const botToken = await getParameter(botTokenSsmPath, {
    decrypt: true,
    maxAge: 300, // 5-minute cache
  });

  const result = await sendTelegramMessage({
    botToken, chatId, text: message, parseMode: 'MarkdownV2',
  });

  if (!result.success) {
    metrics.addMetric('TelegramNotificationFailed', MetricUnit.Count, 1);
    throw new Error(result.errorMessage); // Triggers SQS retry
  }

  metrics.addMetric('TelegramNotificationSent', MetricUnit.Count, 1);
}

The error handling strategy is deliberate:

  • Invalid JSON / failed Zod validation / unknown event type - skip (don’t retry, it’ll never work)
  • Missing chat ID / SSM failure / Telegram API error - throw (retry via SQS, might be transient)
  • After 3 retries - message moves to DLQ for investigation (we can replay later)

Bot Token Security: SSM Parameter Store

The bot token is the only secret in the entire system. We store it in SSM Parameter Store as a SecureString and retrieve it with Lambda Powertools’ built-in caching:

tsx
import { getParameter } from '@aws-lambda-powertools/parameters/ssm';

const botToken = await getParameter('/sfe/develop/telegram/bot-token', {
  decrypt: true,
  maxAge: 300, // Cache for 5 minutes
});

This gives us:

  • Encryption at rest via KMS
  • IAM-scoped access — Lambda can only read its specific parameter
  • Built-in caching — no custom cache logic, no cold-start penalty after first call
  • No environment variable exposure — token never appears in CloudFormation, logs, or error messages

CDK Infrastructure: A Clean Nested Stack

The entire Telegram notification infrastructure lives in a single CDK nested stack, conditionally deployed when enabled:

tsx
// In the stateless stack
if (props.shared.telegramIntegration.enabled) {
  new TelegramNotificationServiceNestedStack(
    this, 'TelegramNotificationServiceStack', {
      shared: { ...props.shared },
      env, stateless: props.stateless,
      codeDeployApplication: serviceLambdaApplication,
      snsTopic, eventBus,
      monitoringSnsTopic: props.monitoringSnsTopic,
      monitoring: props.monitoring,
    },
  );
}

The nested stack creates:

  • SQS Queue with DLQ (3 retries, 14-day retention in prod).
  • EventBridge Rule routing domain events to the queue.
  • Notification Processor Lambda (SQS event source, batch 10, 5s window).
  • Alarm Forwarder Lambda (SNS subscription).
  • IAM permissions scoped to the exact SSM parameter ARN.
  • CloudWatch alarms on queue age and DLQ depth.
tsx
// IAM Least-privilege SSM access
const ssmParameterArn = `arn:aws:ssm:${region}:${account}:parameter${telegramIntegration.botTokenSsmPath}`;

const ssmPolicy = new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ['ssm:GetParameter'],
  resources: [ssmParameterArn],
});

telegramNotificationProcessor.lambda.addToRolePolicy(ssmPolicy);
telegramAlarmForwarder.lambda.addToRolePolicy(ssmPolicy);

Each Lambda can only read the one parameter it needs.

GitHub Actions: Pipeline Notifications Without AWS

For deployment notifications (both success and failure), we don’t even need Lambda. A single curl command in the workflow replaces the entire SES email step:

yaml
-name: Send Telegram notification
if: always()
continue-on-error:true
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
  run:|
    COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1 | cut -c1-72)
    STATUS="${{ steps.status.outputs.result }}"
    EMOJI="${{ steps.status.outputs.emoji }}"

    curl --max-time 10 -s -X POST \\
      "<https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage>" \\
      --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \\
      --data-urlencode "parse_mode=Markdown" \\
      --data-urlencode "text=${EMOJI} *${STATUS}* — \\`develop\\`

    *Workflow:* ${{ github.workflow }}
    *Commit:* \\`$(echo ${{ github.sha }} | cut -c1-7)\\` ${COMMIT_MSG}
    *Actor:* ${{ github.actor }}
    [View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"

What we added:

  • ✅ One curl command
  • ✅ Two GitHub secrets
  • continue-on-error: true (notification failure never blocks deployment)

The Alarm Forwarder: CloudWatch to Telegram

CloudWatch alarms arrive via SNS. The Alarm Forwarder Lambda parses the alarm structure and forwards it to a dedicated Telegram group:

tsx
const forwardTelegramAlarmsAdapter: SNSHandler = async (event: SNSEvent) => {
  for (const record of event.Records) {
    const alarmMessage = JSON.parse(record.Sns.Message);

    // Graceful handling — malformed messages don't throw
    if (!alarmMessage.AlarmName || !alarmMessage.NewStateValue) {
      logger.warn('Malformed alarm notification, skipping');
      continue;
    }

    const formattedMessage = formatAlarmNotification(alarmMessage);

    const botToken = await getParameter(botTokenSsmPath, {
      decrypt: true, maxAge: 300,
    });

    const result = await sendTelegramMessage({
      botToken,
      chatId: alarmsChatId,
      text: formattedMessage,
      parseMode: 'MarkdownV2',
    });

    if (!result.success) {
      throw new Error(`Telegram API error:${result.errorMessage}`);
    }

    metrics.addMetric('TelegramAlarmForwarded', MetricUnit.Count, 1);
  }
};

The alarm formatter produces messages like this below, which format perfectly when displayed in the Telegram app:

🚨 *CloudWatch Alarm*

*Alarm:* develop\\-telegram\\-notification\\-queue\\-age
*State:* ALARM
*Reason:* Threshold crossed: 1 out of 1 datapoints \\[350\\.0\\] was \\>\\= threshold \\(300\\.0\\)
*Time:* 2026\\-05\\-17T10:30:00\\.000Z

Cost Comparison 💰

Let’s compare the actual costs:

Component

Slack Pipeline (per 1,000 notifications)

Telegram Pipeline (per 1,000 notifications)

Lambda execution

~$0.001

~$0.001

SQS messages

~$0.0004

~$0.0004

EventBridge events

~$0.001

~$0.001

SES email sending

~$0.10

$0

Slack Pro (per user/month)

$8.75+ x 3 People

$0

Telegram Bot API

N/A

$0

Total (infra only)

~$0.11

~$0.0024

Total (with Slack license, 3 users)

~$26.35/month

~$0.0024

The infrastructure cost difference is modest, but when you factor in Slack licensing for the team members who need notification access, Telegram saves significantly. That is only 3 people, but with a small startup of say 10-15 people, the costs are approximately $87.50-$131.25.

What We Gained by Switching

After running both systems in parallel for a week before cutting over:

  1. Latency dropped from ~15s to < 1s — no email routing delay.
  2. Better mobile experience — Telegram notifications are instant, lightweight, and don’t mix with work chat.
  3. Richer formatting — MarkdownV2 with emoji, bold, monospace, and links.
  4. Zero cost — Telegram Bot API is free with generous rate limits (30 msg/sec).
  5. Simpler GitHub Actions — one curl command vs AWS credential setup + SES call.

When Slack Still Makes Sense

To be fair, Telegram isn’t always the right choice:

  • Team collaboration — if your team already lives in Slack and needs threaded discussions on alerts, keep Slack.
  • Enterprise compliance — some organisations mandate Slack for audit trails, or you may already be paying for a teams license.
  • Rich integrations — Slack’s app ecosystem (PagerDuty, Jira, etc.) is unmatched.
  • Interactive workflows — Slack’s Block Kit enables buttons, modals, and approval flows.

But for one-way business notifications for a startup— the “something happened, here’s what” pattern, Telegram is simpler, faster, and free.

Wrapping Up 📝

We’ve covered a lot of ground in this article:

The key takeaways:

  1. Telegram Bot API is free and fast — a single HTTPS POST replaces an entire SES email pipeline with sub-second delivery.
  2. Hexagonal architecture keeps it clean — the Telegram adapter is a secondary adapter, formatters are use cases, and Lambda handlers are primary adapters.
  3. SQS provides the safety net — partial batch failures, DLQ, and retry semantics mean notifications are eventually delivered.
  4. SSM Parameter Store for secrets — encrypted at rest, IAM-scoped, cached in memory, never logged.
  5. CDK nested stacks for isolation — conditionally deployed, independently testable, zero impact on existing infrastructure.
  6. GitHub Actions notifications are trivial — one curl command, two secrets, no AWS credentials needed.
  7. Event-driven architecture enables this — the notification system subscribes to domain events without coupling to the business logic that produces them.

The existing Slack infrastructure stays in place for future use (hopefully, we will grow the team in time, and Slack may make sense again). The Telegram integration is purely additive, i.e., a new notification channel that’s faster, cheaper, and simpler to operate.

I hope you found this article useful. If you have any questions or feedback, feel free to reach out!

Ready to level up your AWS skills?

Visit sign-up today and join a community of builders and architects dedicated to mastering the cloud.

Study From Experts

Connect With Us

© 2026 Study From Experts

All rights reserved