A resilient Incident Management System for ingesting high-volume failure signals, debouncing noisy component failures, creating workflow-driven incidents, and closing incidents only after a complete Root Cause Analysis (RCA).
Live Demo: ims.hitanshu.xyz β hosted on an AWS EC2 t3.medium instance (2 vCPU, 4 GB RAM)
This repository is structured as:
server/: Bun + Express backend, BullMQ worker, PostgreSQL/MongoDB/Redis integrationsclient/: React + Vite dashboarddocker-compose.yml: full local stack for Postgres, MongoDB, Redis, backend, and frontend
graph TD
subgraph Client["React + Vite Dashboard"]
UI["Live Feed / RCA / Chaos UI"]
Topology["Topology Map"]
WSClient["WebSocket Client"]
end
subgraph Server["Bun + Express Backend"]
API["REST API"]
Health["GET /health"]
RateLimiter["Ingestion Rate Limiter"]
RingBuffer["In-Memory Ring Buffer"]
Debouncer["Component Debouncer<br/>100 signals or 10 seconds"]
Queue["BullMQ Queue"]
Worker["Async Worker"]
FSM["State Pattern<br/>OPEN -> INVESTIGATING -> RESOLVED -> CLOSED"]
Strategy["Alert Strategy Pattern"]
WSServer["WebSocket Telemetry"]
end
subgraph Data["Persistence and Cache"]
Postgres["PostgreSQL<br/>Work Items + RCA + MTTR"]
Mongo["MongoDB<br/>Raw Signal Audit Log + Timeseries Aggregation"]
Redis["Redis<br/>BullMQ + Dashboard Cache + Counters"]
end
subgraph Optional["Optional Integrations"]
Discord["Discord Webhook Alerts"]
OpenRouter["OpenRouter AI RCA Drafts"]
end
UI --> API
Topology --> API
WSClient <--> WSServer
API --> RateLimiter
RateLimiter --> RingBuffer
RingBuffer --> Debouncer
RingBuffer --> Queue
Debouncer --> Queue
Queue --> Worker
Worker --> Mongo
Worker --> Postgres
Worker --> Redis
Worker --> Strategy
FSM --> Postgres
Strategy -.-> Discord
API -.-> OpenRouter
API --> Health
| Layer | Choice | Why |
|---|---|---|
| Runtime/API | Bun + Express | Fast TypeScript runtime with simple HTTP routing |
| Async jobs | BullMQ + Redis | Durable async processing, retry, backoff, and worker concurrency |
| Source of truth | PostgreSQL | Transactional work item and RCA records |
| Data lake | MongoDB | High-volume raw signal payload storage and aggregation pipeline support |
| Hot path/cache | Redis | Queue backend, dashboard cache, counters, and dropped-signal metrics |
| Frontend | React + Vite | Responsive dashboard with fast local development |
| Validation | Zod | Runtime validation for ingestion and RCA payloads |
| Tests | Bun test | Fast backend unit tests |
| Purpose | Store | Implementation |
|---|---|---|
| Raw signal audit log | MongoDB | Signal documents in server/models/Signal.ts |
| Work items | PostgreSQL | work_items table in server/db/init.sql |
| RCA records | PostgreSQL | rca table with generated mttr_seconds |
| Queue state | Redis | BullMQ queue in server/queue/producer.ts |
| Dashboard cache | Redis | getDashboardState / setDashboardState in server/db/redis.ts |
| Timeseries aggregation | MongoDB | /api/dashboard/timeseries aggregation pipeline |
PostgreSQL remains the source of truth for incident workflow state. MongoDB stores all raw error payloads for auditability and detail views.
The ingestion path is intentionally split so the API does not synchronously write every signal to every database:
POST /api/signalsvalidates the payload and appliesexpress-rate-limit.- Accepted signals enter a preallocated in-memory ring buffer.
- If the ring buffer is full, the API returns
503 Service Unavailableand increments a Redis dropped-signal metric. - A drain loop pulls up to 200 signals every 100ms and enqueues raw persistence jobs to BullMQ.
- The debouncer groups signals by
component_id; it flushes once the bucket reaches 100 signals or the 10-second timer expires. - The worker stores raw signals in MongoDB and creates or updates one PostgreSQL work item for the grouped component.
This design protects the API event loop and persistence layer during bursts. The default ring buffer capacity is 50,000, the debounce threshold is 100, and the debounce window is 10,000ms; these are configurable through environment variables.
Work items follow this lifecycle:
OPEN -> INVESTIGATING -> RESOLVED -> CLOSED
Additional supported paths:
INVESTIGATING -> OPEN
RESOLVED -> INVESTIGATING
Rules enforced by the backend:
OPEN -> CLOSEDandOPEN -> RESOLVEDare rejected.- RCA submission is accepted only when the work item is
RESOLVED. - Closing a work item is rejected if the RCA is missing or incomplete.
- RCA requires
incident_start,incident_end,root_cause_category,fix_applied, andprevention_steps. incident_endmust be afterincident_start.- MTTR is calculated by PostgreSQL as
incident_end - incident_startinrca.mttr_seconds.
The State Pattern is implemented in server/workflow/state.ts. Alert routing uses the Strategy Pattern in server/workflow/strategy.ts.
The React dashboard includes:
- Summary cards for incident counts and operational metrics
- Live feed sorted by priority or time
- Incident detail panel with current state
- Raw signal log view from MongoDB
- RCA form with category dropdown and text areas
- State transition actions for acknowledge, resolve, submit RCA, and close
- Topology map and chaos simulator views
- WebSocket updates for work item and throughput events
- Optional AI-assisted RCA draft generation when
OPENROUTER_API_KEYis configured
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Health and basic runtime metrics |
POST |
/api/signals |
Ingest one signal |
GET |
/api/work-items |
List work items with pagination and optional filters |
GET |
/api/work-items/:id |
Fetch one work item |
PATCH |
/api/work-items/:id/transition |
Move a work item through the state machine |
GET |
/api/work-items/:id/signals |
Fetch linked raw signals from MongoDB |
POST |
/api/work-items/:id/rca |
Submit an RCA for a resolved work item |
GET |
/api/work-items/:id/rca |
Fetch a submitted RCA |
POST |
/api/work-items/:id/rca/draft |
Generate an optional AI RCA draft |
GET |
/api/dashboard/summary |
Dashboard counts, MTTR, MTTA, and top components |
GET |
/api/dashboard/timeseries |
MongoDB signal aggregation over time |
POST |
/api/dashboard/ai-summary |
Optional AI dashboard summary |
Example signal payload:
{
"signal_id": "0de7a3c5-08a1-4de9-aadc-4b37e879d302",
"component_id": "PG_PROD_01",
"component_type": "RDBMS",
"severity": "CRITICAL",
"message": "Connection timeout error: pool exhausted",
"timestamp": "2026-05-03T10:00:00.000Z",
"payload": {
"source": "chaos_simulator"
}
}- Docker and Docker Compose
- Bun
- Node.js/npm for the frontend when running outside Docker
make up
make logsDocker endpoints:
- Frontend:
http://localhost:5173 - Backend API:
http://localhost:3001 - Health check:
http://localhost:3001/health
Stop containers without deleting volumes:
make downReset all databases:
make nukeStart only the databases:
make infraInstall dependencies:
make installRun backend and frontend locally:
make devLocal endpoints:
- Frontend:
http://localhost:5173 - Backend API:
http://localhost:5555 - Health check:
http://localhost:5555/health
The backend validates configuration at startup in server/config.ts.
| Variable | Default | Purpose |
|---|---|---|
PORT |
5555 locally, 3001 in Docker Compose |
Backend port |
DATABASE_URL |
Required | PostgreSQL connection string |
MONGODB_URL |
Required | MongoDB connection string |
REDIS_URL |
Required | Redis connection string |
RING_BUFFER_CAPACITY |
50000 |
In-memory signal buffer size |
DEBOUNCE_WINDOW_MS |
10000 |
Component debounce window |
DEBOUNCE_THRESHOLD |
100 |
Signals required for immediate debounce flush |
WORKER_CONCURRENCY |
5 |
BullMQ worker concurrency |
RATE_LIMIT_WINDOW_MS |
60000 |
Ingestion rate-limit window |
RATE_LIMIT_MAX |
1000 |
Max ingestion requests per window |
DISABLE_RATE_LIMIT |
false |
Disable rate limiting (useful for hammer burst testing) |
OPENROUTER_API_KEY |
unset | Enables AI RCA draft and summary endpoints |
DISCORD_WEBHOOK_URL |
unset | Enables outbound P0/P1 alert delivery |
For local development, create server/.env if you want optional integrations:
OPENROUTER_API_KEY="your_openrouter_key"
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."The sample incident script is server/scripts/simulate-incident.ts. It sends a database outage, an API cascade, and a cache pressure event.
Run it against the local backend on port 5555:
make simulateRun it against the Docker backend on port 3001:
make simulate-dockerRun it against any deployed backend, including an AWS URL:
API_URL=https://your-api.example.com/api/signals make simulateAPI_URL can be either the API base URL or the full ingestion endpoint. These are equivalent:
API_URL=https://your-api.example.com make simulate
API_URL=https://your-api.example.com/api/signals make simulateThe script prints the target ingestion endpoint before sending events, reports accepted/rejected/failed counts, and exits non-zero if no signals are accepted.
To satisfy the 10,000 signals/sec ingestion burst requirement, the system relies on an in-memory ring buffer. Rather than allowing infinite queue growth which leads to OOM crashes, the API sheds load gracefully.
Running a concurrent burst test of 60,000 signals demonstrates this exact behavior. The API successfully ingests to capacity and instantly applies pushback (HTTP 503) to protect the Node event loop, all while sustaining over 15,000 req/sec throughput.
Note on Environment: This test was run natively on Linux, where Docker binds directly to the kernel's network stack. Running this on macOS/Windows via Docker Desktop will artificially cap throughput at ~6,000 req/sec due to virtualized network bridge overhead.
π¨ HAMMER TEST - Burst Verification
============================================================
Target URL: http://localhost:3001/api/signals
Burst Size: 60,000 requests
Concurrency: 10000
============================================================
β³ Firing burst...
Progress: 100.0% (60000/60000)
π RESULTS
============================================================
Total Requests: 60,000
Successful (202): 56,886 (94.81%)
Backpressure (503): 3,114 (5.19%)
Rate Limited (429): 0 (0.00%)
Total Duration: 3797.09ms
Actual Rate: 15,802 req/sec
β
VERIFICATION
============================================================
Burst Rate (β₯10k/sec): β
PASS (15802 req/sec)
Backpressure Active (503s): β
PASS (3114 responses)
π HAMMER TEST PASSED - System handles 10k/sec burst with backpressure
Running the Hammer Test:
# Start databases
make infra
# Start the backend locally with rate limiting disabled
DISABLE_RATE_LIMIT=true make dev-server
# In another terminal, run the hammer test
cd server && bun run hammer -- --burst-size 60000 --concurrent 10000The hammer script (server/scripts/hammer.ts) is a dedicated tooling artifact that explicitly verifies the 10k/sec burst requirement by:
- Firing a controlled burst of requests at maximum concurrency
- Tracking success/failure rates and response times
- Verifying backpressure behavior (503 responses when buffer is full)
- Providing clear pass/fail metrics for reviewers
Run all backend tests:
make testRun one test file:
make test-file F=state
make test-file F=dbRetryCurrent test coverage includes:
- Ring buffer enqueue, drain, and overflow behavior
- Debouncer threshold and timer flush behavior
- PostgreSQL retry helper behavior for transient and non-transient errors
- State Pattern transitions and mandatory RCA guard
- API workflow coverage for invalid transitions, RCA status guards, incomplete RCA rejection, valid RCA submission, MTTR response, and close flow
- RCA schema validation, required fields, valid categories, and date ordering
Latest local verification:
60 pass
0 fail
Ran 60 tests across 6 files.
/healthreturns status, uptime, total accepted signals, and current throughput window.- Throughput is logged every 5 seconds as
Signals/sec: <rate> | Dropped: <count>. - Dropped signals are tracked in Redis when the ring buffer rejects load.
- BullMQ jobs retry with exponential backoff.
- PostgreSQL route queries retry known transient failures such as connection drops, deadlocks, and serialization failures.
- State transitions use conditional PostgreSQL updates and reject stale concurrent updates with
409 Conflict. - Graceful shutdown closes the worker and queue.
| Layer | Scope | Mechanism | Config |
|---|---|---|---|
| BullMQ | Worker jobs | Job-level exponential backoff | 3 attempts, 1s β 2s β 4s (producer.ts) |
query() helper |
REST API routes | Per-query transient error retry | 2 retries, 500ms β 1s (db/postgres.ts) |
Transient errors (PG codes 08006, 40P01, 57P01, 40001; Node codes ECONNRESET, ECONNREFUSED) trigger automatic retry. Non-transient errors (unique violations 23505, schema mismatches 23502) are wrapped in UnrecoverableError to skip retry entirely.
Concurrent state transition false success β The state machine used a conditional WHERE id = $2 AND state = $3 update but did not check rowCount. A stale concurrent request could report success even after another request had already transitioned the work item. Fixed by checking rowCount, rolling back on zero updated rows, and returning 409 Conflict. Unit test covers this path.
MongoDB bulk insert collisions β When the debouncer flushed a batch to the BullMQ worker, duplicate signal_ids caused the entire insertMany to fail. Fixed by using { ordered: false } and catching MongoBulkWriteError, allowing unique signals to persist while safely ignoring duplicates.
AI RCA output shape mismatches β The LLM occasionally returned ["Step 1", "Step 2"] arrays for prevention_steps instead of a string, failing Zod validation. Fixed with a normalization layer in the frontend: Array.isArray(val) ? val.join("\n") : val before populating the form.
State machine bypass via API β An engineer could submit an RCA and force-close a work item while it was still OPEN, bypassing the investigation workflow. Fixed by adding explicit state guards: POST /rca rejects unless state is RESOLVED, and PATCH /transition physically blocks invalid edges.
Ephemeral RCA draft data loss β AI-generated drafts stored in React state were lost on navigation or refresh. Fixed with localStorage keyed by incident.id, auto-saving on change and purging only on successful backend submission.
Docker volume persistence after rename β Renaming the database in docker-compose.yml still threw database "ims" does not exist because the old volume persisted. Fixed with docker-compose down -v to purge stale volume state.
- The ring buffer is in-memory. Accepted signals that have not yet reached BullMQ can be lost if the process crashes.
- The CLI simulation script defaults to
http://localhost:5555, but can target Docker or deployed environments throughmake simulate-dockerorAPI_URL=... make simulate. - Optional AI and Discord integrations require external credentials and are disabled by default.
| Command | Description |
|---|---|
make up |
Build and start the full Docker stack |
make down |
Stop containers and preserve volumes |
make nuke |
Stop containers and delete volumes |
make logs |
Tail Docker logs |
make infra |
Start only Postgres, MongoDB, and Redis |
make install |
Install backend and frontend dependencies |
make dev-server |
Run the backend locally |
make dev-client |
Run the frontend locally |
make dev |
Run backend and frontend locally |
make test |
Run all backend unit tests |
make test-file F=<name> |
Run one backend test file |
make simulate |
Run the sample chaos simulation against local backend 5555 or custom API_URL |
make simulate-docker |
Run the sample chaos simulation against Docker backend 3001 |
make hammer |
Run the hammer burst test to verify 10k/sec requirement |
make clean |
Remove local dependencies and build artifacts |