Aws Scheduler

AWS EventBridge Scheduler is a fully managed, serverless service on AWS that allows you to create, run, and manage scheduled tasks at scale. Brighter integrates with AWS EventBridge Scheduler to provide cloud-native scheduling for AWS workloads.

When to Use AWS Scheduler

AWS Scheduler is recommended when:

  • Running on AWS: Your application is deployed on AWS (EC2, ECS, Lambda, etc.)

  • Serverless Architecture: You prefer managed services over self-hosted solutions

  • AWS Native: You want to leverage AWS services directly

  • Scalability: You need to handle millions of scheduled messages

  • Cost Optimization: You want to pay per use without managing infrastructure

  • Multi-Region: You need to schedule across AWS regions

When NOT to use:

  • Multi-Cloud: Need scheduler to work across cloud providers

  • On-Premises: Application runs outside AWS

  • Complex Scheduling: Need advanced scheduling features (job chains, dependencies)

  • Dashboard: Need visual management interface

How Brighter Integrates with AWS Scheduler

Brighter provides two approaches for scheduling with AWS EventBridge Scheduler:

When UseMessageTopicAsTarget = true (default), Brighter schedules messages directly to the target SNS topic or SQS queue:

Your Code → CommandProcessor.SendAsync(delay, command)

Brighter creates AWS EventBridge Schedule

Schedule stored in AWS EventBridge Scheduler

[Schedule fires at specified time]

AWS EventBridge → Directly publishes to target SNS/SQS

Your Dispatcher → Handler executes

Benefits:

  • Fewer moving parts

  • Lower latency (no intermediate handler)

  • Simpler architecture

2. Via FireAwsScheduler Message

When UseMessageTopicAsTarget = false or using request scheduler, Brighter schedules through an intermediate FireAwsScheduler message:

Your Code → CommandProcessor.SendAsync(delay, command)

Brighter creates AWS EventBridge Schedule

Schedule stored in AWS EventBridge Scheduler

[Schedule fires at specified time]

AWS EventBridge → Publishes FireAwsScheduler to scheduler topic/queue

AwsSchedulerFiredHandler executes

CommandProcessor dispatches original command

Your Handler executes

Use when:

  • Request scheduling (Send/Publish commands, not messages)

  • Target topic doesn't exist at scheduling time

  • Need centralized scheduler message handling

IAM Role Requirements

AWS EventBridge Scheduler requires an IAM role that allows it to publish to SNS topics and send messages to SQS queues.

Trust Policy

The role must trust the EventBridge Scheduler service:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "scheduler.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

Permissions Policy

The role must have permissions to publish messages:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:SendMessage",
                "sns:Publish"
            ],
            "Resource": ["*"]
        }
    ]
}

Best Practice: Limit Resource to specific topic/queue ARNs instead of "*".

Automatic Role Creation

Brighter can create the role automatically if it doesn't exist:

var schedulerFactory = new AwsSchedulerFactory(awsConnection, "my-scheduler-role")
{
    MakeRole = OnMissingRole.Create  // Brighter will create role if missing
};

Options:

  • OnMissingRole.Assume (default): Assume role exists, throw error if not found

  • OnMissingRole.Create: Create role if it doesn't exist

NuGet Packages

AWS Scheduler integration is available in two versions:

dotnet add package Paramore.Brighter.MessageScheduler.AWS.V4

Uses the latest AWS SDK for .NET v4.

AWS SDK v3 (Legacy)

dotnet add package Paramore.Brighter.MessageScheduler.AWS

Uses AWS SDK for .NET v3 (deprecated in AWS).

Recommendation: Use V4 for new projects. Both packages provide identical Brighter functionality.

Configuration

Basic Configuration

Configure AWS Scheduler with minimal settings:

using Microsoft.Extensions.Hosting;
using Paramore.Brighter.Extensions.DependencyInjection;
using Paramore.Brighter.MessageScheduler.AWS.V4;
using Paramore.Brighter.MessagingGateway.AWSSQS;

var builder = WebApplication.CreateBuilder(args);

// Create AWS connection
var awsConnection = new AWSMessagingGatewayConnection(
    credentials,  // From AWS credentials provider
    RegionEndpoint.USEast1
);

// Configure Brighter with AWS Scheduler
builder.Services.AddBrighter(options =>
{
    options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new AwsSchedulerFactory(awsConnection, "my-scheduler-role")
{
    // Schedule topic for FireAwsScheduler messages (when not direct to target)
    SchedulerTopicOrQueue = new RoutingKey("message-scheduler-topic"),
    OnConflict = OnSchedulerConflict.Overwrite
})
.AutoFromAssemblies();

var app = builder.Build();

Configuration with Dispatcher (FireAwsScheduler Handler)

If using FireAwsScheduler approach, configure the Dispatcher to handle scheduler messages:

using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection;
using Paramore.Brighter.MessagingGateway.AWSSQS.V4;

var builder = WebApplication.CreateBuilder(args);

var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.USEast1);

// Configure producer registry (includes scheduler topic)
var producerRegistry = new SnsProducerRegistryFactory(
    awsConnection,
    [
        new SnsPublication
        {
            Topic = new RoutingKey("message-scheduler-topic"),
            RequestType = typeof(FireAwsScheduler)
        }
    ]
).Create();

// Configure subscriptions (handle FireAwsScheduler messages)
var subscriptions = new[]
{
    new SqsSubscription<FireAwsScheduler>(
        new SubscriptionName("scheduler-message-subscription"),
        new ChannelName("scheduler-message-channel"),
        new RoutingKey("message-scheduler-topic"),
        bufferSize: 10,
        timeOut: TimeSpan.FromSeconds(30),
        messagePumpType: MessagePumpType.Proactor
    )
};

builder.Services.AddServiceActivator(options =>
{
    options.Subscriptions = subscriptions;
})
.UseExternalBus(configure =>
{
    configure.ProducerRegistry = producerRegistry;
})
.UseScheduler(new AwsSchedulerFactory(awsConnection, "my-scheduler-role")
{
    SchedulerTopicOrQueue = new RoutingKey("message-scheduler-topic"),
    UseMessageTopicAsTarget = false,  // Use FireAwsScheduler approach
    OnConflict = OnSchedulerConflict.Overwrite
})
.AutoFromAssemblies();

var app = builder.Build();

Configuration with Scheduler Groups

AWS EventBridge Scheduler supports organizing schedules into groups:

var schedulerFactory = new AwsSchedulerFactory(awsConnection, "my-scheduler-role")
{
    Group = new SchedulerGroup
    {
        Name = "orders-scheduler-group",
        Tags = new List<Tag>
        {
            new Tag { Key = "Environment", Value = "Production" },
            new Tag { Key = "Application", Value = "OrderProcessing" }
        },
        MakeSchedulerGroup = OnMissingSchedulerGroup.Create  // Create group if missing
    }
};

Options:

  • OnMissingSchedulerGroup.Assume (default): Assume group exists

  • OnMissingSchedulerGroup.Create: Create group if it doesn't exist

Configuration with Flexible Time Window

EventBridge Scheduler supports flexible time windows for cost optimization:

var schedulerFactory = new AwsSchedulerFactory(awsConnection, "my-scheduler-role")
{
    FlexibleTimeWindowMinutes = 5  // Execute within 5-minute window
};

A flexible time window allows AWS to batch executions for better efficiency. The schedule will execute within the specified window after the scheduled time.

Configuration with Custom Scheduler ID

Customize how Brighter generates scheduler IDs for messages and requests:

var schedulerFactory = new AwsSchedulerFactory(awsConnection, "my-scheduler-role")
{
    // Custom scheduler ID for messages
    GetOrCreateMessageSchedulerId = message => $"msg-{message.Id}",

    // Custom scheduler ID for requests (idempotency)
    GetOrCreateRequestSchedulerId = request =>
    {
        if (request is ProcessOrderCommand cmd)
        {
            return $"order-{cmd.OrderId}";  // Idempotent per order
        }
        return request.Id.ToString();
    }
};

Use Cases:

  • Idempotency: Same ID prevents duplicate schedules

  • Tracking: Correlate scheduled tasks with business entities

  • Debugging: Meaningful IDs in AWS Console

Code Examples

Basic Scheduling with Delay

Schedule a command to execute after a delay:

public class OrderService
{
    private readonly IAmACommandProcessor _commandProcessor;

    public async Task CreateOrder(Order order)
    {
        await _repository.SaveAsync(order);

        // Schedule payment processing for 30 minutes later
        var schedulerId = await _commandProcessor.SendAsync(
            TimeSpan.FromMinutes(30),
            new ProcessPaymentCommand { OrderId = order.Id, Amount = order.Total }
        );

        _logger.LogInformation(
            "Scheduled payment processing for order {OrderId}: {SchedulerId}",
            order.Id,
            schedulerId
        );
    }
}

Scheduling with Absolute Time

Schedule for a specific time:

public class ReportService
{
    private readonly IAmACommandProcessor _commandProcessor;

    public async Task ScheduleDailyReport()
    {
        // Schedule for tomorrow at 9 AM UTC
        var tomorrow9AM = DateTimeOffset.UtcNow.Date.AddDays(1).AddHours(9);

        var schedulerId = await _commandProcessor.SendAsync(
            tomorrow9AM,
            new GenerateDailyReportCommand { Date = DateTime.UtcNow.Date }
        );

        _logger.LogInformation("Scheduled daily report: {SchedulerId}", schedulerId);
    }
}

Cancelling a Scheduled Job

Cancel a scheduled task before it executes:

public class OrderService
{
    private readonly IAmACommandProcessor _commandProcessor;
    private readonly IMessageScheduler _scheduler;

    public async Task CancelOrder(Guid orderId)
    {
        var order = await _repository.GetAsync(orderId);

        // Cancel scheduled payment
        if (!string.IsNullOrEmpty(order.PaymentSchedulerId))
        {
            await _scheduler.CancelAsync(order.PaymentSchedulerId);
            _logger.LogInformation(
                "Cancelled scheduled payment for order {OrderId}",
                orderId
            );
        }

        order.Status = OrderStatus.Cancelled;
        await _repository.UpdateAsync(order);
    }
}

Publishing Events with Delay

Schedule an event to be published:

public class TrialService
{
    private readonly IAmACommandProcessor _commandProcessor;

    public async Task StartTrial(User user)
    {
        user.TrialStartDate = DateTime.UtcNow;
        user.TrialEndDate = DateTime.UtcNow.AddDays(30);
        await _repository.SaveAsync(user);

        // Schedule trial expiry reminder for 7 days before end
        var reminderDate = user.TrialEndDate.AddDays(-7);
        var schedulerId = await _commandProcessor.PublishAsync(
            reminderDate,
            new TrialExpiringEvent { UserId = user.Id, DaysRemaining = 7 }
        );

        _logger.LogInformation(
            "Scheduled trial expiry reminder for user {UserId}: {SchedulerId}",
            user.Id,
            schedulerId
        );
    }
}

Scheduling Modes Comparison

Feature
Direct to Target
Via FireAwsScheduler

Configuration

UseMessageTopicAsTarget = true

UseMessageTopicAsTarget = false

Use Case

Message scheduling

Request scheduling

Latency

Lower (direct)

Higher (2-hop)

Components

Fewer

More (requires Dispatcher)

Flexibility

Target must exist

More flexible

Recommended For

Production messages

Requests and commands

Comparison with Other Schedulers

Feature
AWS Scheduler
Quartz.NET
Hangfire
InMemory

Cloud Native

AWS Only

Serverless

Persistence

AWS Managed

Database

Database

None

Dashboard

AWS Console

Limited

Yes

No

Cancellation

Reschedule

Cost Model

Pay-per-use

Infrastructure

Infrastructure

Free

Setup Complexity

Moderate (IAM)

Moderate

Easy

Minimal

Production Ready

Multi-Cloud

Strong Naming

When to use AWS Scheduler:

  • Running on AWS infrastructure

  • Prefer serverless/managed services

  • Need high scalability

  • Cost-effective pay-per-use model

Best Practices

1. Use Direct to Target for Messages

// Good - Direct scheduling (default)
var schedulerFactory = new AwsSchedulerFactory(awsConnection, "my-role")
{
    UseMessageTopicAsTarget = true  // Default, can omit
};
// Bad - Unnecessary indirection for messages
var schedulerFactory = new AwsSchedulerFactory(awsConnection, "my-role")
{
    UseMessageTopicAsTarget = false  // Forces FireAwsScheduler approach
};

2. Limit IAM Role Permissions

// Good - Specific resource ARNs
{
    "Effect": "Allow",
    "Action": ["sqs:SendMessage", "sns:Publish"],
    "Resource": [
        "arn:aws:sqs:us-east-1:123456789012:my-queue",
        "arn:aws:sns:us-east-1:123456789012:my-topic"
    ]
}
// Bad - Overly permissive
{
    "Effect": "Allow",
    "Action": ["sqs:*", "sns:*"],
    "Resource": ["*"]
}

3. Use Scheduler Groups for Organization

// Good - Organized by feature/team
var schedulerFactory = new AwsSchedulerFactory(awsConnection, "role")
{
    Group = new SchedulerGroup
    {
        Name = "payment-processing-schedules",
        Tags = new List<Tag>
        {
            new Tag { Key = "Team", Value = "Payments" },
            new Tag { Key = "CostCenter", Value = "Engineering" }
        }
    }
};

4. Handle OnConflict Appropriately

// Good - Explicit conflict handling
var schedulerFactory = new AwsSchedulerFactory(awsConnection, "role")
{
    OnConflict = OnSchedulerConflict.Overwrite,  // Or Throw, depending on use case
    GetOrCreateRequestSchedulerId = request => $"idempotent-{request.Id}"
};

5. Use Custom Scheduler IDs for Idempotency

// Good - Idempotent based on business key
GetOrCreateRequestSchedulerId = request =>
{
    if (request is ProcessPaymentCommand cmd)
    {
        return $"payment-{cmd.TransactionId}";  // One schedule per transaction
    }
    return request.Id.ToString();
}
// Bad - Always creates new schedule
GetOrCreateRequestSchedulerId = request => Guid.NewGuid().ToString();

6. Monitor AWS Scheduler Metrics

// Use AWS CloudWatch to monitor:
// - InvocationAttempts
// - InvocationSuccessRate
// - InvocationThrottles
// - TargetErrorRate

// Set up CloudWatch alarms for failures

7. Use Flexible Time Windows for Cost Optimization

// Good - Allow flexibility for non-critical schedules
var schedulerFactory = new AwsSchedulerFactory(awsConnection, "role")
{
    FlexibleTimeWindowMinutes = 5  // AWS can batch executions
};

8. Test with LocalStack

// Good - Test AWS Scheduler locally with LocalStack
var awsConnection = new AWSMessagingGatewayConnection(
    credentials,
    RegionEndpoint.USEast1,
    cfg =>
    {
        if (environment.IsDevelopment())
        {
            cfg.ServiceURL = "http://localhost:4566";  // LocalStack
        }
    }
);

Troubleshooting

Schedules Not Executing

Symptom: Schedules created but never fire

Possible Causes:

  1. IAM role missing or incorrect permissions

  2. Target topic/queue doesn't exist

  3. Schedule created in wrong region

  4. FireAwsScheduler handler not configured

Solutions:

// 1. Verify IAM role permissions in AWS Console
// 2. Ensure target exists before scheduling
var schedulerFactory = new AwsSchedulerFactory(awsConnection, "role")
{
    MakeRole = OnMissingRole.Create  // Auto-create role
};

// 3. Verify region matches your resources
var awsConnection = new AWSMessagingGatewayConnection(
    credentials,
    RegionEndpoint.USEast1  // Match your SNS/SQS region
);

// 4. Configure FireAwsScheduler subscription if needed

Access Denied Errors

Symptom: AccessDeniedException when creating schedules

Cause: Application credentials lack EventBridge Scheduler permissions

Solution:

{
    "Effect": "Allow",
    "Action": [
        "scheduler:CreateSchedule",
        "scheduler:DeleteSchedule",
        "scheduler:GetSchedule",
        "scheduler:UpdateSchedule",
        "iam:PassRole"
    ],
    "Resource": "*"
}

Schedule Conflicts

Symptom: ConflictException when creating schedule

Cause: Schedule with same ID already exists

Solutions:

// Option 1: Overwrite existing schedule
OnConflict = OnSchedulerConflict.Overwrite

// Option 2: Throw exception and handle in code
OnConflict = OnSchedulerConflict.Throw

High Costs

Symptom: Unexpected AWS Scheduler costs

Causes:

  • Too many one-time schedules created and not cleaned up

  • Short polling intervals

Solutions:

// 1. Use flexible time windows
FlexibleTimeWindowMinutes = 5

// 2. Clean up old schedules
await _scheduler.CancelAsync(oldSchedulerId);

// 3. Consider Quartz/Hangfire for very frequent schedules
// AWS Scheduler is billed per schedule invocation

Migration from Other Schedulers

From InMemory Scheduler

// Before (Development)
services.AddBrighter(options => { ... })
    .UseScheduler(new InMemorySchedulerFactory())
    .AutoFromAssemblies();

// After (Production on AWS)
services.AddBrighter(options => { ... })
    .UseScheduler(new AwsSchedulerFactory(awsConnection, "scheduler-role")
    {
        SchedulerTopicOrQueue = new RoutingKey("scheduler-topic"),
        OnConflict = OnSchedulerConflict.Overwrite
    })
    .AutoFromAssemblies();

No code changes required - just swap the scheduler factory!

From Quartz or Hangfire to AWS Scheduler

// Before (Quartz)
services.AddBrighter(options => { ... })
    .UseScheduler(provider =>
    {
        var schedulerFactory = provider.GetRequiredService<ISchedulerFactory>();
        return new QuartzSchedulerFactory(
            schedulerFactory.GetScheduler().GetAwaiter().GetResult()
        );
    })
    .AutoFromAssemblies();

// After (AWS Scheduler)
services.AddBrighter(options => { ... })
    .UseScheduler(new AwsSchedulerFactory(awsConnection, "scheduler-role")
    {
        SchedulerTopicOrQueue = new RoutingKey("scheduler-topic")
    })
    .AutoFromAssemblies();

Benefits of moving to AWS Scheduler:

  • No database required

  • No server maintenance

  • Automatic scaling

  • Pay-per-use pricing

Summary

AWS EventBridge Scheduler is a cloud-native, serverless scheduling solution perfect for:

  • AWS Workloads - Native integration with AWS services

  • Serverless Applications - No infrastructure to manage

  • High Scalability - Millions of schedules supported

  • Cost Optimization - Pay only for what you use

Recommended for production when running on AWS infrastructure. Consider Quartz.NET or Hangfire for multi-cloud or on-premises deployments.

Key Decision Factors:

  • ✅ Use AWS Scheduler if: Running on AWS, prefer managed services, need scalability

  • ✅ Use Quartz if: Multi-cloud, complex scheduling, need job clustering

  • ✅ Use Hangfire if: Need dashboard, simple setup, OK without strong naming

  • ✅ Use InMemory if: Testing only, not for production

Last updated

Was this helpful?