> For the complete documentation index, see [llms.txt](https://brightercommand.gitbook.io/paramore-brighter-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://brightercommand.gitbook.io/paramore-brighter-documentation/scheduler/brighterschedulersupport/inmemoryscheduler.md).

# InMemory Scheduler

The InMemory Scheduler is a lightweight, timer-based scheduling implementation provided by Brighter for testing, development, and demonstration purposes.

> **Reference** · Applies to **Brighter V10**

The **InMemory Scheduler** is a lightweight, timer-based scheduling implementation provided by Brighter for **testing, development, and demonstration purposes**. It requires no external dependencies and stores scheduled jobs in memory using timers.

## Important Warning

**The InMemory Scheduler is NOT durable and is NOT recommended for production use.**

* **No Persistence**: All scheduled jobs are lost if the application crashes or restarts
* **No Distribution**: Cannot be shared across multiple application instances
* **No Recovery**: Failed jobs are not automatically retried after restart
* **Memory Bound**: All scheduled jobs are held in memory

**Use this scheduler for:**

* Unit and integration tests
* Local development
* Demos and proof-of-concepts
* Production scenarios where losing scheduled work is acceptable

## What is the InMemory Scheduler?

The InMemory Scheduler creates its timers from a .NET `TimeProvider` — `TimeProvider.System` unless you set `InMemorySchedulerFactory.TimeProvider`. When you schedule a message:

1. Brighter creates an in-memory timer for the specified delay, with `TimeProvider.CreateTimer`
2. The timer fires at the scheduled time
3. Brighter dispatches your message to the appropriate handler
4. The timer is removed from memory

This simple approach makes it perfect for testing but unsuitable for production systems that require durability.

## InMemory Scheduler Architecture

```
Your Code
    ↓
CommandProcessor.SendAsync(delay, command)
    ↓
InMemoryScheduler
    ↓
TimeProvider.CreateTimer(delay)
    ↓
[Timer stored in memory]
    ↓
[Timer fires after delay]
    ↓
CommandProcessor dispatches command
    ↓
Your Handler executes
```

## When to Use InMemory Scheduler

### Recommended Use Cases

**Unit Testing:**

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;
using Xunit;

[Fact]
public async Task Should_Schedule_Command_For_Later_Execution()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddBrighter(options =>
        {
            // ...
        })
        .UseScheduler(new InMemorySchedulerFactory())  // Perfect for tests
        .AutoFromAssemblies();

    var provider = services.BuildServiceProvider();
    var commandProcessor = provider.GetRequiredService<IAmACommandProcessor>();

    // Act
    var schedulerId = await commandProcessor.SendAsync(
        TimeSpan.FromMilliseconds(100),
        new TestCommand()
    );

    // Assert
    Assert.NotNull(schedulerId);
    await Task.Delay(150);  // Wait for scheduled execution
    // Verify command was handled...
}
```

### Limited Production Scenarios

The InMemory Scheduler might be acceptable in production **only if**:

* Loss of scheduled work is acceptable (non-critical notifications, analytics, etc.)
* Your application rarely restarts
* Scheduled work has short delays (minutes, not hours/days)
* You have alternative mechanisms to recover lost work

**Example - Acceptable Production Use:**

```csharp
using System;
using System.Threading.Tasks;
using Paramore.Brighter;

// Low-priority analytics events that can be lost
public class AnalyticsService(IAmACommandProcessor commandProcessor, IAnalyticsRepository repository)
{
    public async Task TrackUserAction(string userId, string action)
    {
        // Track immediately
        await repository.SaveActionAsync(userId, action);

        // Schedule low-priority aggregation (acceptable to lose)
        await commandProcessor.SendAsync(
            TimeSpan.FromMinutes(5),
            new AggregateAnalyticsCommand { UserId = userId }
        );
    }
}
```

## InMemory Scheduler Configuration

### InMemory Scheduler Factory Options

`InMemorySchedulerFactory` takes no constructor arguments, so `new InMemorySchedulerFactory()` is a complete configuration; these four properties are what you can change about it.

| Option                          | Type                     | Default                        | Description                                                                                            |
| ------------------------------- | ------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `TimeProvider`                  | `TimeProvider`           | `TimeProvider.System`          | The clock the scheduler measures delays against, and the seam a `FakeTimeProvider` replaces in a test. |
| `GetOrCreateMessageSchedulerId` | `Func<Message, string>`  | a fresh identifier per message | Names the timer Brighter creates for a message.                                                        |
| `GetOrCreateRequestSchedulerId` | `Func<IRequest, string>` | a fresh identifier per request | Names the timer Brighter creates for a request.                                                        |
| `OnConflict`                    | `OnSchedulerConflict`    | `Throw`                        | What Brighter does when a timer with the same identifier already exists.                               |

`OnSchedulerConflict` is `Throw` or `Overwrite`.

### Basic Configuration

Configure the InMemory Scheduler with `InMemorySchedulerFactory`:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Brighter.Extensions.DependencyInjection;
using Paramore.Brighter;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddBrighter(options =>
{
    options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new InMemorySchedulerFactory())  // Add InMemory Scheduler
.AutoFromAssemblies();

var app = builder.Build();
```

### Environment-Specific Configuration

Use InMemory for development, production schedulers elsewhere:

```csharp
using Hangfire;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;
using Paramore.Brighter.MessageScheduler.Hangfire;

var builder = WebApplication.CreateBuilder(args);

var brighter = builder.Services.AddBrighter(options =>
{
    options.HandlerLifetime = ServiceLifetime.Scoped;
});

// UseScheduler needs one type that is both a message and a request scheduler factory,
// so choose the concrete factory here; a helper could only return one of the two interfaces
if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Testing"))
{
    brighter.UseScheduler(new InMemorySchedulerFactory());
}
else
{
    // Production - use a durable scheduler; Hangfire's storage is configured with AddHangfire
    brighter.UseScheduler(provider =>
    {
        // Resolve Hangfire's client first: that initialises its storage (Brighter#4600)
        var client = provider.GetRequiredService<IBackgroundJobClientV2>();
        return new HangfireMessageSchedulerFactory { Client = client };
    });
}

brighter.AutoFromAssemblies();
```

### Controlling Time in Tests

The scheduler measures every delay against its `TimeProvider`, so a test can replace the clock rather than wait for it. `FakeTimeProvider`, from the `Microsoft.Extensions.TimeProvider.Testing` package, only moves when you call `Advance`:

```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Time.Testing;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;

var timeProvider = new FakeTimeProvider();

var services = new ServiceCollection();
services.AddBrighter()
    .UseScheduler(new InMemorySchedulerFactory { TimeProvider = timeProvider })
    .AsyncHandlers(registry => registry.RegisterAsync<SendReminder, SendReminderHandler>());

var commandProcessor = services.BuildServiceProvider()
    .GetRequiredService<IAmACommandProcessor>();

await commandProcessor.SendAsync(TimeSpan.FromMinutes(5), new SendReminder());

timeProvider.Advance(TimeSpan.FromMinutes(5));  // SendReminderHandler has run when this returns

public class SendReminder() : Command(Id.Random());

public class SendReminderHandler : RequestHandlerAsync<SendReminder>
{
    public override Task<SendReminder> HandleAsync(SendReminder command, CancellationToken cancellationToken = default)
    {
        // Your reminder logic here
        return base.HandleAsync(command, cancellationToken);
    }
}
```

`Advance` runs any timer that falls due on the calling thread, so the handler has run by the time the call returns, and the test needs no `Task.Delay`. Until you advance the clock, nothing fires: advancing it by four minutes and fifty-nine seconds leaves the command unhandled, and real time passing does not move a `FakeTimeProvider` at all.

The handler is registered with `AsyncHandlers`, which works at every 10.x release. `AutoFromAssemblies()` works too from Brighter 10.8.0; at 10.7.0 it registers the scheduler's own handlers twice — see [Registering Handlers When You Schedule Requests](/paramore-brighter-documentation/scheduler/schedulingamessage.md#registering-handlers-when-you-schedule-requests).

## InMemory Scheduler NuGet Package

The InMemory Scheduler has no package of its own: `InMemoryScheduler` and `InMemorySchedulerFactory` are in `Paramore.Brighter`, and `UseScheduler` comes with the dependency-injection package you already use to configure Brighter:

```bash
dotnet add package Paramore.Brighter.Extensions.DependencyInjection
```

## InMemory Scheduler Code Examples

### Basic Scheduling

Schedule a command with a delay:

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;

public class OrderService(
    IAmACommandProcessor commandProcessor, IOrderRepository repository, ILogger<OrderService> logger)
{
    public async Task CreateOrder(Order order)
    {
        await repository.SaveAsync(order);

        // Schedule order confirmation email for 5 minutes later
        var schedulerId = await commandProcessor.SendAsync(
            TimeSpan.FromMinutes(5),
            new SendOrderConfirmationCommand { OrderId = order.Id }
        );

        logger.LogInformation("Scheduled confirmation email: {SchedulerId}", schedulerId);
    }
}
```

### Scheduling with Absolute Time

Schedule for a specific time:

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;

public class ReportService(IAmACommandProcessor commandProcessor, ILogger<ReportService> logger)
{
    public async Task ScheduleDailyReport()
    {
        // Schedule for tomorrow at 9 AM
        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

From Brighter 10.8.0 an id returned by `SendAsync`, `PublishAsync` or `PostAsync` with a delay, like `schedulerId` in the examples above, can be cancelled or rescheduled through `IAmARequestSchedulerAsync`. At 10.7.0 it could not: the command processor created a new scheduler for each call, so `CancelAsync` returned and the request still ran ([#4437](https://github.com/BrighterCommand/Brighter/issues/4437)). On 10.7.0, schedule through the scheduler when you need to cancel; this works at 10.8.0 too:

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;

public class OrderService(
    IAmARequestSchedulerAsync scheduler, IOrderRepository repository, ILogger<OrderService> logger)
{
    public async Task CreateOrder(Order order)
    {
        // Schedule through the scheduler, so that it can cancel the request later
        order.ConfirmationSchedulerId = await scheduler.ScheduleAsync(
            new SendOrderConfirmationCommand { OrderId = order.Id },
            RequestSchedulerType.Send,
            TimeSpan.FromMinutes(5));

        await repository.SaveAsync(order);
    }

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

        // Cancel the scheduled confirmation email
        if (!string.IsNullOrEmpty(order.ConfirmationSchedulerId))
        {
            await scheduler.CancelAsync(order.ConfirmationSchedulerId);
            logger.LogInformation("Cancelled scheduled email for order {OrderId}", orderId);
        }

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

### Testing with InMemory Scheduler

Example unit test using InMemory Scheduler. `TestCommandHandler` records each command it handles in a `TestHandlerRegistry`, which the test registers as a singleton and then asks:

```csharp
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;
using Xunit;

public class SchedulingTests : IDisposable
{
    private readonly ServiceProvider _serviceProvider;
    private readonly IAmACommandProcessor _commandProcessor;
    private readonly TestHandlerRegistry _handlerRegistry;

    public SchedulingTests()
    {
        var services = new ServiceCollection();
        _handlerRegistry = new TestHandlerRegistry();

        services.AddSingleton(_handlerRegistry);

        services.AddBrighter(options =>
        {
            options.HandlerLifetime = ServiceLifetime.Scoped;
        })
        .UseScheduler(new InMemorySchedulerFactory())  // InMemory for tests
        .AsyncHandlers(registry => registry.RegisterAsync<TestCommand, TestCommandHandler>());

        _serviceProvider = services.BuildServiceProvider();
        _commandProcessor = _serviceProvider.GetRequiredService<IAmACommandProcessor>();
    }

    [Fact]
    public async Task Should_Execute_Scheduled_Command_After_Delay()
    {
        // Arrange
        var command = new TestCommand();
        var delay = TimeSpan.FromMilliseconds(100);

        // Act
        var schedulerId = await _commandProcessor.SendAsync(delay, command);

        // Assert - command not yet executed
        Assert.False(_handlerRegistry.WasHandled(command.Id));

        // Wait for scheduled execution
        await Task.Delay(delay.Add(TimeSpan.FromMilliseconds(50)));

        // Assert - command executed
        Assert.True(_handlerRegistry.WasHandled(command.Id));
    }

    [Fact]
    public async Task Should_Cancel_Scheduled_Command()
    {
        // Arrange
        var command = new TestCommand();
        var delay = TimeSpan.FromSeconds(10);  // Long delay
        var scheduler = _serviceProvider.GetRequiredService<IAmARequestSchedulerAsync>();

        // Act - schedule through the scheduler that cancels it
        var schedulerId = await scheduler.ScheduleAsync(command, RequestSchedulerType.Send, delay);
        await scheduler.CancelAsync(schedulerId);  // Cancel immediately

        // Wait to ensure it would have executed
        await Task.Delay(TimeSpan.FromSeconds(11));

        // Assert - command was NOT executed
        Assert.False(_handlerRegistry.WasHandled(command.Id));
    }

    public void Dispose()
    {
        _serviceProvider?.Dispose();
    }
}

public class TestHandlerRegistry
{
    private readonly ConcurrentDictionary<Id, bool> _handled = new();

    public void Record(Id id) => _handled[id] = true;
    public bool WasHandled(Id id) => _handled.ContainsKey(id);
    public int HandledCount => _handled.Count;
}

public class TestCommandHandler(TestHandlerRegistry registry) : RequestHandlerAsync<TestCommand>
{
    public override Task<TestCommand> HandleAsync(TestCommand command, CancellationToken cancellationToken = default)
    {
        registry.Record(command.Id);
        return base.HandleAsync(command, cancellationToken);
    }
}
```

## InMemory Scheduler Best Practices

### 1. Use for Testing Only

```csharp
using Microsoft.Extensions.Hosting;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;

// Good - Environment-specific
if (environment.IsDevelopment() || environment.IsEnvironment("Testing"))
{
    brighter.UseScheduler(new InMemorySchedulerFactory());
}
```

```csharp
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;

// Bad - Always using InMemory in production
brighter.UseScheduler(new InMemorySchedulerFactory());
```

### 2. Document Production Limitations

If you use InMemory in production, document why:

```csharp
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;

// PRODUCTION NOTE: Using InMemory scheduler because loss of
// scheduled analytics aggregations is acceptable. These are
// non-critical and will be regenerated on next sync.
brighter.UseScheduler(new InMemorySchedulerFactory());
```

### 3. Keep Delays Short

If using in production, keep delays under a few minutes:

```csharp
using System;

// Good - Short delay acceptable to lose
await _commandProcessor.SendAsync(TimeSpan.FromMinutes(2), command);

// Bad - Long delay likely to be lost
await _commandProcessor.SendAsync(TimeSpan.FromHours(24), command);
```

### 4. Test Scheduler Behavior

Write tests that verify scheduled behavior. This one is another test method for the `SchedulingTests` class above, and uses its fields:

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

[Fact]
public async Task Should_Handle_Concurrent_Scheduled_Commands()
{
    // Schedule multiple commands with different delays
    var ids = new List<string>();
    for (int i = 0; i < 10; i++)
    {
        ids.Add(await _commandProcessor.SendAsync(
            TimeSpan.FromMilliseconds(50 + i * 10),
            new TestCommand()
        ));
    }

    // Wait for all to execute
    await Task.Delay(TimeSpan.FromMilliseconds(200));

    // Verify all were handled
    Assert.Equal(10, _handlerRegistry.HandledCount);
}
```

### 5. Don't Rely on It for Critical Work

```csharp
using System;

// Bad - Critical payment processing
await _commandProcessor.SendAsync(
    TimeSpan.FromMinutes(5),
    new ProcessPaymentCommand { Amount = 1000.00m }
);

// Good - Critical work should use durable scheduler
await _commandProcessor.SendAsync(
    TimeSpan.FromMinutes(5),
    new ProcessPaymentCommand { Amount = 1000.00m }
);  // With Quartz or Hangfire in production
```

## InMemory Scheduler Troubleshooting

### Scheduled Jobs Not Executing

**Symptom**: Jobs scheduled but never execute

**Possible Causes**:

1. Application stopped before timer fires
2. Delay too short (already passed)
3. Exception in handler preventing execution

**Solution**:

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;
using Xunit;

// Add logging to verify scheduling
var schedulerId = await _commandProcessor.SendAsync(delay, command);
_logger.LogInformation("Scheduled job {SchedulerId} for {Delay}", schedulerId, delay);

// Verify handler is registered
Assert.NotNull(_serviceProvider.GetService<IHandleRequestsAsync<YourCommand>>());
```

### Scheduled Jobs Lost After Restart

**Symptom**: Application restart loses all scheduled jobs

**Cause**: This is expected behavior - InMemory scheduler has no persistence

**Solution**: Use a production scheduler (Quartz, Hangfire) if you need durability

### Memory Usage Growing

**Symptom**: Memory consumption increases over time

**Cause**: Too many scheduled jobs held in memory

**Solution**:

* Reduce number of concurrent scheduled jobs
* Use shorter delays
* Consider a production scheduler with external storage

## Related Documentation

* [Brighter Scheduler Support](/paramore-brighter-documentation/scheduler/brighterschedulersupport.md) - Overview of scheduling in Brighter
* [Switching Schedulers](/paramore-brighter-documentation/scheduler/switchingschedulers.md) - Moving to or from this scheduler
* [Quartz Scheduler](/paramore-brighter-documentation/scheduler/brighterschedulersupport/quartzscheduler.md) - Production scheduler with persistence
* [Hangfire Scheduler](/paramore-brighter-documentation/scheduler/brighterschedulersupport/hangfirescheduler.md) - Production scheduler with dashboard
* [AWS Scheduler](/paramore-brighter-documentation/scheduler/brighterschedulersupport/awsscheduler.md) - Cloud-native AWS scheduling
* [Azure Scheduler](/paramore-brighter-documentation/scheduler/brighterschedulersupport/azurescheduler.md) - Cloud-native Azure scheduling

## InMemory Scheduler Summary

The InMemory Scheduler is a lightweight, zero-dependency scheduling solution perfect for:

* **Unit and integration tests** - No external dependencies
* **Local development** - Fast and simple
* **Demos and POCs** - Quick to set up

**NOT recommended for production** due to:

* **No persistence** - Jobs lost on restart
* **No distribution** - Single-process only
* **No recovery** - No durability guarantees

Use production schedulers (Quartz.NET, Hangfire, AWS Scheduler, Azure Service Bus Scheduler) for any system requiring durability and reliability.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://brightercommand.gitbook.io/paramore-brighter-documentation/scheduler/brighterschedulersupport/inmemoryscheduler.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
