tl;dr AzureEventGridSimulator lets you run Azure Event Grid locally so you can develop and test event-driven code without an Azure subscription. This post walks through installing it, wiring up a local Azure Function, publishing events, filtering them, and using the dashboard to see what’s going on.

I created this article, but it has been reviewed and refined with help from AI tools: Claude and Grammarly.

Back in 2018, I wrote about the hoops you had to jump through to locally debug an Event Grid triggered Azure Function. It involved Postman, a magic request header, and a fair bit of friction. Azure Event Grid still doesn’t have an official local emulator, so I built one. It’s now at v5 and supports CloudEvents, multiple subscriber types, event filtering, retry with dead-lettering, a built-in dashboard, and deployment via Docker or .NET Aspire.

This post is a hands-on walkthrough. By the end you’ll have events flowing from curl through the simulator into a local Azure Function, with filtering and the dashboard proving it all works.

What you’ll need

You don’t need Docker for this walkthrough. We’ll use the dotnet tool installation, which is the simplest way to get up and running.

Installing the simulator

1
dotnet tool install --global AzureEventGridSimulator

Run it to confirm it installed:

1
azure-eventgrid-simulator

You should see it start up and complain about missing configuration - that’s fine, we’ll set that up next. Hit Ctrl+C to stop it for now.

The simulator can also run as a Docker container or via .NET Aspire.

Setting up an Azure Function

We need something to receive events. Let’s create a minimal Azure Function with an Event Grid trigger.

1
2
3
func init EventGridDemo --worker-runtime dotnet-isolated --target-framework net10.0
cd EventGridDemo
func new --name ProcessEvent --template "EventGridTrigger"

Open the generated ProcessEvent.cs and update it to look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Azure.Messaging.EventGrid;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace EventGridDemo;

public class ProcessEvent
{
    private readonly ILogger<ProcessEvent> _logger;

    public ProcessEvent(ILogger<ProcessEvent> logger)
    {
        _logger = logger;
    }

    [Function(nameof(ProcessEvent))]
    public void Run([EventGridTrigger] EventGridEvent eventGridEvent)
    {
        _logger.LogInformation("Event type: {Type}", eventGridEvent.EventType);
        _logger.LogInformation("Event subject: {Subject}", eventGridEvent.Subject);
        _logger.LogInformation("Event data: {Data}", eventGridEvent.Data);
    }
}

We’re using EventGridEvent here because we’ll be publishing events in Event Grid schema format, and the simulator delivers them to subscribers as-is. If you prefer CloudEvents, you can set "inputSchema": "CloudEventV1_0" on the topic and use the CloudEvent type from Azure.Messaging instead.

Before starting, open local.settings.json and update the storage setting. The template defaults to UseDevelopmentStorage=true which requires Azurite to be running. For this walkthrough we don’t need it:

1
2
3
4
5
6
7
8
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "",
        "AzureWebJobsSecretStorageType": "Files",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
    }
}

Start it:

1
func start

You should see it listening on http://localhost:7071. Leave this running in its own terminal.

Configuring the simulator

Create an appsettings.json in a separate directory where you’ll run the simulator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "topics": [
    {
      "name": "UserEvents",
      "port": 60101,
      "key": "TheLocal+DevelopmentKey=",
      "subscribers": {
        "http": [
          {
            "name": "ProcessEventFunction",
            "endpoint": "http://localhost:7071/runtime/webhooks/EventGrid?functionName=ProcessEvent",
            "disableValidation": true
          }
        ]
      }
    }
  ]
}

Each topic gets its own HTTPS port. The key works just like the real Event Grid - events must include it in an aeg-sas-key header. Setting disableValidation to true skips the subscription validation handshake so we don’t have to deal with that during local dev.

The subscriber types are keyed by kind: http, serviceBus, storageQueue, and eventHub. We’re using http here. The wiki covers the full configuration reference.

Publishing your first event

If you haven’t already trusted the .NET development certificate:

1
dotnet dev-certs https --trust

Start the simulator from the directory containing your appsettings.json:

1
azure-eventgrid-simulator

Open another terminal and publish an event:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
curl -k -X POST \
  "https://localhost:60101/api/events?api-version=2018-01-01" \
  -H "Content-Type: application/json" \
  -H "aeg-sas-key: TheLocal+DevelopmentKey=" \
  -d '[{
    "id": "1",
    "subject": "/users/signup",
    "data": {
      "username": "jane.doe",
      "email": "[email protected]"
    },
    "eventType": "User.SignedUp",
    "eventTime": "2026-04-04T10:00:00Z",
    "dataVersion": "1"
  }]'

Note: Events are sent as a JSON array, even for a single event. The -k flag accepts the self-signed dev certificate.

Now check your terminals. In the simulator terminal you should see it log the received event and forward it. In the Azure Functions terminal you should see:

1
2
3
Event type: User.SignedUp
Event subject: /users/signup
Event data: {"username":"jane.doe","email":"[email protected]"}

That’s the basic flow working. Let’s make it more interesting.

Checking the dashboard

The simulator has a built-in dashboard that shows events and their delivery status in real time. It’s enabled by default. Open your browser and go to:

1
https://localhost:60101/dashboard

You should see the event you just published, along with its delivery status to the ProcessEventFunction subscriber. Publish a few more events with different subjects or event types and watch them appear. This is the quickest way to see what’s happening without jumping between terminal windows.

Filtering events

In production, you’d use Event Grid filtering so subscribers only receive events they care about. Let’s try that. Stop the simulator and update appsettings.json to add a second subscriber with a filter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "topics": [
    {
      "name": "UserEvents",
      "port": 60101,
      "key": "TheLocal+DevelopmentKey=",
      "subscribers": {
        "http": [
          {
            "name": "AllEvents",
            "endpoint": "http://localhost:7071/runtime/webhooks/EventGrid?functionName=ProcessEvent",
            "disableValidation": true
          },
          {
            "name": "SignupsOnly",
            "endpoint": "http://localhost:7071/runtime/webhooks/EventGrid?functionName=ProcessEvent",
            "disableValidation": true,
            "filter": {
              "includedEventTypes": ["User.SignedUp"],
              "subjectBeginsWith": "/users/"
            }
          }
        ]
      }
    }
  ]
}

Restart the simulator and publish two events - one that matches the filter and one that doesn’t:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl -k -X POST \
  "https://localhost:60101/api/events?api-version=2018-01-01" \
  -H "Content-Type: application/json" \
  -H "aeg-sas-key: TheLocal+DevelopmentKey=" \
  -d '[{
    "id": "2",
    "subject": "/users/signup",
    "data": { "username": "alice" },
    "eventType": "User.SignedUp",
    "eventTime": "2026-04-04T10:01:00Z",
    "dataVersion": "1"
  }]'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl -k -X POST \
  "https://localhost:60101/api/events?api-version=2018-01-01" \
  -H "Content-Type: application/json" \
  -H "aeg-sas-key: TheLocal+DevelopmentKey=" \
  -d '[{
    "id": "3",
    "subject": "/orders/created",
    "data": { "orderId": 42 },
    "eventType": "Order.Created",
    "eventTime": "2026-04-04T10:02:00Z",
    "dataVersion": "1"
  }]'

Now open the dashboard at https://localhost:60101/dashboard. You should see that:

  • The AllEvents subscriber received both events
  • The SignupsOnly subscriber only received the first event (the signup) and the order event was filtered out

The simulator dashboard showing two events with filtering applied. The signup event was delivered to both subscribers while the order event was only delivered to AllEvents.

That’s basic filtering using event type and subject prefix. The simulator also supports advanced filtering with up to 25 expressions per subscriber - operators like NumberGreaterThan, StringContains, BoolEquals, and more, targeting any property in the event payload.

Retry and dead-lettering

What happens when a subscriber is down? Try it - stop your Azure Function, then publish another event. Check the dashboard and you’ll see the delivery status change to show retry attempts in progress.

Retry is enabled by default and follows Azure’s exponential backoff pattern - 10 seconds, 30 seconds, 1 minute, then progressively longer intervals up to 12 hours between attempts. If a subscriber returns a 400, 401, 403, or 413, the event is immediately dead-lettered with no retry.

You can configure retry and dead-lettering per subscriber:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "name": "MySubscriber",
  "endpoint": "http://localhost:7071/runtime/webhooks/EventGrid?functionName=ProcessEvent",
  "disableValidation": true,
  "retryPolicy": {
    "maxDeliveryAttempts": 5,
    "eventTimeToLiveInMinutes": 60
  },
  "deadLetter": {
    "enabled": true,
    "folderPath": "./dead-letters"
  }
}

When an event exhausts its retries or exceeds its time-to-live, it’s written as a JSON file to the dead-letter folder. You can inspect the files to see exactly what failed. Start your Azure Function again and the queued events will be delivered on the next retry attempt.

The retry and dead-letter wiki page has the full retry schedule and HTTP status code handling details.

Publishing from .NET code

In a real project you’d publish events from your application code, not curl. The official Azure.Messaging.EventGrid NuGet package works against the simulator with no changes. Install it with:

1
dotnet add package Azure.Messaging.EventGrid

Then publish an event:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
using Azure;
using Azure.Messaging.EventGrid;

var client = new EventGridPublisherClient(
    new Uri("https://localhost:60101/api/events"),
    new AzureKeyCredential("TheLocal+DevelopmentKey="));

var @event = new EventGridEvent(
    subject: "/orders/created",
    eventType: "Order.Created",
    dataVersion: "1",
    data: new { OrderId = 42, Total = 99.95 });

await client.SendEventAsync(@event);

This is the exact same EventGridPublisherClient you’d use against a real Event Grid topic. Swap the URI and key via configuration and your code works in both environments.

What else can it do?

This walkthrough covered HTTP webhook subscribers with the EventGrid schema, but there’s more to explore:

Wrapping Up

We went from nothing to a working local Event Grid setup - publishing events, receiving them in an Azure Function, proving that filters work using the dashboard, and seeing how retry and dead-lettering behave when a subscriber goes down. All without touching Azure.

The full documentation is on the GitHub wiki. If you run into any issues or have feature requests, raise an issue.

Thanks for reading.