Use this guide to troubleshoot Server-Sent Events (SSE) issues.
Quick Path (80% of issues)
// turbo
- •
Test SSE endpoint:
bashcurl -N -H "Accept: text/event-stream" http://localhost:5000/api/events
Should see:
data: {"type":"Connected",...} - •
Check MartenCommitListener has case for your projection
- •
Check QueryInvalidationService maps notification to query keys
- •
Check ReactiveQuery uses matching
queryKeys
If still broken, continue to full debugging below.
Symptoms
- •✗ Frontend doesn't update after mutation
- •✗
ReactiveQuerydoesn't invalidate - •✗ SSE connection fails or disconnects
- •✗ Events not received in browser
Related Skills
Prerequisites:
- •
/start-solution- Solution must be running to debug SSE
First Steps:
- •
/verify-feature- Run basic checks (build, tests) before deep debugging
Related Debugging:
- •
/debug-cache- If issue seems cache-related instead of SSE - •
/doctor- Check if environment setup is correct
After Fixing:
- •
/verify-feature- Confirm fix works - •
/scaffold-test- Add tests to prevent regression
Debugging Steps
1. Verify SSE Endpoint is Working
Test the SSE endpoint directly:
# Terminal 1: Connect to SSE stream curl -N -H "Accept: text/event-stream" http://localhost:5000/api/events
You should see:
: connected
data: {"type":"Connected","timestamp":"2026-01-15T20:00:00Z"}
If connection fails:
- •✗ Check API service is running
- •✗ Verify
/api/eventsendpoint exists inEventsEndpoint.cs - •✗ Check firewall/network issues
2. Verify Notifications Are Defined
Check src/Shared/BookStore.Shared/Notifications/DomainEventNotifications.cs:
// ✅ Correct - implements IDomainEventNotification public record BookCreatedNotification(Guid Id, string Title) : IDomainEventNotification; // ✗ Wrong - missing interface public record BookCreatedNotification(Guid Id, string Title);
If notification is missing:
- •Create notification in
DomainEventNotifications.cs - •Implement
IDomainEventNotificationinterface - •Include all data needed for frontend invalidation
3. Verify MartenCommitListener Configuration
Open src/BookStore.ApiService/Infrastructure/MartenCommitListener.cs:
Check if your projection has a handler:
private async Task ProcessDocumentChangeAsync(
IDocumentChange change,
CancellationToken cancellationToken)
{
switch (change)
{
case BookProjection proj:
await HandleBookChangeAsync(proj, cancellationToken);
break;
// ❌ Missing: Your projection case
case AuthorProjection proj:
await HandleAuthorChangeAsync(proj, cancellationToken);
break;
}
}
Check if handler sends notification:
private async Task HandleBookChangeAsync(
BookProjection book,
CancellationToken cancellationToken)
{
var notification = new BookUpdatedNotification(book.Id, book.Title);
// ✅ Correct - sends notification
await _notificationService.NotifyAsync(notification, cancellationToken);
// ✗ Wrong - forgot to send
// (no NotifyAsync call)
}
If handler is missing:
- •Add case for your projection
- •Create handler method that calls
NotifyAsync - •Use appropriate notification type
4. Verify QueryInvalidationService Mapping
Open src/Web/BookStore.Web/Services/QueryInvalidationService.cs:
Check if notification maps to query keys:
public IEnumerable<string> GetInvalidationKeys(IDomainEventNotification notification)
{
return notification switch
{
BookCreatedNotification => new[] { "Books" },
BookUpdatedNotification => new[] { "Books" },
// ❌ Missing: Your notification
AuthorUpdatedNotification => new[] { "Authors" },
_ => Array.Empty<string>()
};
}
If mapping is missing:
- •Add case for your notification type
- •Return query keys that should be invalidated
- •Match keys used in
ReactiveQuerysetup
5. Verify Frontend EventsService
Check browser console for SSE connection:
In Chrome DevTools:
- •Open Network tab
- •Look for "events" request (type: eventsource)
- •Check status is "pending" (active connection)
- •View "EventStream" tab to see events
If connection is closed:
- •Check
EventsService.StartListening()is called inOnInitializedAsync - •Verify base URL is correct
- •Check for JavaScript errors
6. Verify ReactiveQuery Configuration
Check component using ReactiveQuery:
// ✅ Correct - query keys match invalidation mapping
bookQuery = new ReactiveQuery<PagedListDto<BookDto>>(
queryFn: FetchBooksAsync,
eventsService: EventsService,
invalidationService: InvalidationService,
queryKeys: new[] { "Books" }, // Matches QueryInvalidationService
onStateChanged: StateHasChanged,
logger: Logger
);
// ✗ Wrong - query keys don't match
queryKeys: new[] { "AllBooks" } // Doesn't match "Books"
If query doesn't invalidate:
- •Ensure
queryKeysmatchQueryInvalidationServicemapping - •Verify
EventsServiceis subscribed - •Check
onStateChangedcallback is provided
7. Test End-to-End
Perform a mutation and watch the flow:
# Terminal 1: Watch SSE stream
curl -N -H "Accept: text/event-stream" http://localhost:5000/api/events
# Terminal 2: Trigger mutation
curl -X POST http://localhost:5000/api/admin/books \
-H "Content-Type: application/json" \
-d '{"title":"Test Book",...}'
Expected flow:
- •Command executed
- •Event stored in Marten
- •
MartenCommitListenertriggered - •Notification sent via SSE
- •Browser receives event
- •
QueryInvalidationServicemaps to keys - •
ReactiveQueryinvalidates - •Query refetches
- •UI updates
If any step fails, locate where:
- •Check logs in Aspire dashboard
- •Add debug logging to
MartenCommitListener - •Use browser console to see received events
Common Issues & Fixes
Issue: Events Not Sent
Symptom: MartenCommitListener not triggered
Fix:
- •Ensure
MartenCommitListeneris registered in DI - •Check Marten event store configuration
- •Verify projection lifecycle (
InlinevsAsync)
Issue: Wrong Event Type
Symptom: Notification sent but frontend doesn't invalidate
Fix:
// Check notification type name matches exactly case "BookUpdatedNotification": // ✅ Correct case "BookUpdated": // ✗ Wrong
Issue: Multiple Tabs Don't Update
Symptom: Updates only visible in tab that made change
Fix:
- •SSE works per-connection, each tab needs own connection
- •Each tab should call
EventsService.StartListening() - •Verify SignalR isn't being used (project uses SSE)
Issue: SSE Connection Drops
Symptom: Connection works then stops
Fix:
- •Check server-side timeout configuration
- •Verify no proxy/load balancer kills long connections
- •Add reconnection logic in
EventsService
Verification Checklist
- • SSE endpoint accessible at
/api/events - • Notification class implements
IDomainEventNotification - •
MartenCommitListenerhas handler for projection - • Handler calls
NotifyAsyncwith notification - •
QueryInvalidationServicemaps notification to keys - • Frontend
ReactiveQueryuses matching query keys - •
EventsService.StartListening()called on mount - • Browser DevTools shows active EventSource connection
- • End-to-end test confirms UI updates after mutation
Debugging Tools
Backend:
- •Aspire Dashboard → Structured Logs → Filter by "notification"
- •Add logging in
MartenCommitListener:_logger.LogInformation("Sending {Type}", notification.GetType().Name)
Frontend:
- •Browser Console → Look for EventSource logs
- •React DevTools → Check component re-renders
- •Network tab → Verify EventSource connection
Related Skills
First Steps:
- •
/verify-feature- Run basic checks (build, tests) before deep debugging
Related Debugging:
- •
/debug-cache- If issue seems cache-related instead of SSE - •
/doctor- Check if environment setup is correct
After Fixing:
- •
/verify-feature- Confirm fix works - •
/scaffold-test- Add tests to prevent regression
See Also:
- •scaffold-write - SSE notification setup in MartenCommitListener
- •scaffold-frontend-feature - Frontend SSE integration
- •real-time-notifications - SSE architecture and data flow
- •ApiService AGENTS.md - Backend notification patterns
- •Web AGENTS.md - Frontend SSE patterns