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:
1. Direct to Target (Recommended)
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 executesBenefits:
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 executesUse 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 foundOnMissingRole.Create: Create role if it doesn't exist
NuGet Packages
AWS Scheduler integration is available in two versions:
AWS SDK v4 (Recommended)
dotnet add package Paramore.Brighter.MessageScheduler.AWS.V4Uses the latest AWS SDK for .NET v4.
AWS SDK v3 (Legacy)
dotnet add package Paramore.Brighter.MessageScheduler.AWSUses 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 existsOnMissingSchedulerGroup.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
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
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 failures7. 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:
IAM role missing or incorrect permissions
Target topic/queue doesn't exist
Schedule created in wrong region
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 neededAccess 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.ThrowHigh 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 invocationMigration 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
Related Documentation
Brighter Scheduler Support - Overview of scheduling in Brighter
Quartz Scheduler - Alternative for non-AWS environments
Hangfire Scheduler - Alternative with dashboard
InMemory Scheduler - For testing and development
Azure Scheduler - Azure equivalent
AWS SNS Configuration - Configuring AWS messaging
External Links
LocalStack EventBridge - For local testing
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?
