DEV Community

Bassem Hussein
Bassem Hussein

Posted on

🕒 Automating Periodic Tasks with EventBridge Scheduler, .NET, and CloudFormation

Many applications need to perform tasks on a recurring schedule — refreshing data, syncing APIs, or cleaning up records. Instead of managing EC2 cron jobs or containers, AWS gives you a fully managed option: Amazon EventBridge Scheduler.

In this post, we’ll build a simple proof of concept (POC) showing how to trigger a .NET Lambda function on a schedule using AWS CloudFormation.

We’ll also discuss what to do if your job takes longer than the Lambda time limit of 15 minutes.


🧩 What You’ll Build

You’ll deploy:

  • A .NET 8 Lambda function that simulates fetching stock prices
  • A CloudFormation template that defines:
    • The Lambda function and its IAM role
    • An EventBridge Scheduler that runs every 10 minutes
    • An SQS Dead Letter Queue (DLQ) for failed invocations

Here’s the architecture:

Architecture


⚙️ Step 1. Create a .NET Lambda Function

Start by creating a simple .NET 8 Lambda that logs a simulated stock price.

🧱 Project Setup

dotnet new lambda.empty-function -n StockPriceUpdater
cd StockPriceUpdater
Enter fullscreen mode Exit fullscreen mode

🧠 Function Code (Function.cs)

using System.Text.Json;
using Amazon.Lambda.Core;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace StockPriceUpdater;

public class ScheduledJobInput
{
    public string JobType { get; set; } = string.Empty;
    public object? Payload { get; set; }
}

public class Payload
{
    public string AccountCode { get; set; } = string.Empty;
}

public class Function
{
    public async Task InvokeAsync(ScheduledJobInput input, ILambdaContext context)
    {
        context.Logger.LogLine($"[StockPriceUpdater] Triggered for JobType: {input.JobType}");

        if (input.Payload is JsonElement payloadJson)
        {
            var payload = payloadJson.Deserialize<Payload>();
            context.Logger.LogLine($"AccountCode: {payload?.AccountCode}");
        }

        await UpdateStockPricesAsync(input.JobType);
    }

    private Task UpdateStockPricesAsync(string jobType)
    {
        var price = Math.Round(new Random().NextDouble() * 100, 2);
        Console.WriteLine($"[{jobType}] Simulated stock price: ${price} at {DateTime.UtcNow:O}");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

This function accepts a small JSON payload and logs a random “stock price.”

It’s a simple stand-in for any periodic background task you might need.


📜 Step 2. Deploy with CloudFormation

Save the following as stock-price-updater.yml:

AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda triggered by EventBridge Scheduler for periodic stock updates

Parameters:
  ResourcePrefix:
    Type: String
    Default: stock-updater
  LambdaCodeBucket:
    Type: String
    Description: S3 bucket where Lambda zip is stored
  LambdaCodeKey:
    Type: String
    Description: Key (file name) of Lambda zip in S3

Resources:
  ##################################################
  # IAM Role for Lambda
  ##################################################
  StockPriceUpdaterRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ResourcePrefix}-lambda-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  ##################################################
  # Lambda Function
  ##################################################
  StockPriceUpdaterFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${ResourcePrefix}-function"
      Handler: StockPriceUpdater::StockPriceUpdater.Function::InvokeAsync
      Role: !GetAtt StockPriceUpdaterRole.Arn
      Runtime: dotnet8
      Timeout: 600 # 10 minutes (max is 900 = 15 minutes)
      MemorySize: 512
      Code:
        S3Bucket: !Ref LambdaCodeBucket
        S3Key: !Ref LambdaCodeKey

  ##################################################
  # Dead Letter Queue (DLQ)
  ##################################################
  StockPriceUpdaterDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub "${ResourcePrefix}-dlq"
      MessageRetentionPeriod: 1209600 # 14 days

  ##################################################
  # Role for EventBridge Scheduler
  ##################################################
  SchedulerInvokeRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ResourcePrefix}-scheduler-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "scheduler.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: SchedulerInvokeLambdaAndDLQ
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action: "lambda:InvokeFunction"
                Resource: !GetAtt StockPriceUpdaterFunction.Arn
              - Effect: "Allow"
                Action: "sqs:SendMessage"
                Resource: !GetAtt StockPriceUpdaterDLQ.Arn

  ##################################################
  # EventBridge Scheduler
  ##################################################
  StockPriceUpdaterSchedule:
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: !Sub "${ResourcePrefix}-scheduler"
      Description: "Triggers the Lambda every 10 minutes"
      FlexibleTimeWindow:
        Mode: "OFF"
      ScheduleExpression: "rate(10 minutes)"
      State: "ENABLED"
      Target:
        Arn: !GetAtt StockPriceUpdaterFunction.Arn
        RoleArn: !GetAtt SchedulerInvokeRole.Arn
        DeadLetterConfig:
          Arn: !GetAtt StockPriceUpdaterDLQ.Arn
        Input: |
          {
            "JobType": "StockUpdate",
            "Payload": { "AccountCode": "Primary" }
          }

Outputs:
  LambdaFunctionName:
    Value: !Ref StockPriceUpdaterFunction
    Description: Name of the Lambda function
Enter fullscreen mode Exit fullscreen mode

🚀 Step 3. Package and Deploy

  1. Package the Lambda
dotnet lambda package --output-package ./StockPriceUpdater.zip
aws s3 cp ./StockPriceUpdater.zip s3://your-bucket/
Enter fullscreen mode Exit fullscreen mode
  1. Deploy the stack
aws cloudformation deploy \
  --template-file stock-price-updater.yml \
  --stack-name StockPriceUpdater \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides \
      ResourcePrefix=stock-updater \
      LambdaCodeBucket=your-bucket \
      LambdaCodeKey=StockPriceUpdater.zip
Enter fullscreen mode Exit fullscreen mode

🧾 Step 4. Verify It Works

After deployment:

  • Go to Amazon EventBridge → Scheduler → Schedules — you’ll see one schedule.
  • Open CloudWatch Logs for your Lambda and verify logs every 10 minutes:
[StockPriceUpdater] Triggered for JobType: StockUpdate
AccountCode: Primary
[StockUpdate] Simulated stock price: $74.82 at 2025-10-27T09:00:00Z
Enter fullscreen mode Exit fullscreen mode

Failures are sent automatically to the DLQ (SQS).


⚠️ Lambda Execution Time Limit (15 Minutes)

AWS Lambda has a hard timeout limit of 15 minutes per execution.

If your background job might exceed this:

  • Step Functions: Chain multiple Lambdas or tasks with retries and parallel steps.
  • AWS Fargate: Run containerized tasks without worrying about execution time.
  • AWS Batch: Run large-scale batch jobs with automatic compute scaling.

For long-running tasks, replace Lambda with Step Functions, Fargate, or Batch triggered by EventBridge Scheduler.


🧭 Wrapping Up

You learned how to:

  • Use EventBridge Scheduler to trigger a .NET Lambda periodically
  • Handle failures with an SQS DLQ
  • Deploy everything with CloudFormation

This approach is serverless, scalable, and fully managed, ideal for background jobs, periodic updates, or monitoring tasks.


✅ Key Takeaways

Feature Description
EventBridge Scheduler Replaces cron jobs with a managed scheduler
Lambda Ideal for short, frequent tasks
DLQ (SQS) Captures failed events for later inspection
CloudFormation Declarative, repeatable deployments
Limit Max Lambda duration = 15 minutes — use Step Functions, Fargate, or Batch for longer jobs

Top comments (0)