Skip to content

Stop a hunt

Stop a hunt

Sometimes you launch a fleet sweep and immediately realize the indicator is wrong, or coverage is already enough, or a higher-priority hunt needs the concurrent-hunt slot. Stopping is the explicit “I’m done here” control: it flips the hunt’s status to cancelled so the UI stops treating it as live, releases the global concurrent-hunt slot, and lets the timeout watcher exit on its next tick.

What it does

The Stop button (red, top-right of the active hunt detail panel, only visible while the hunt is pending or running) calls POST /api/v1/hunts/{id}/cancel. The endpoint is admin-onlyrequireAdmin runs before any work, so non-admin operators get an HTTP 403 forbidden response and the toast surface displays the error.

The server-side flow is intentionally tight:

  1. Status flip. UPDATE hunts SET status='cancelled', completed_at=NOW() WHERE id=$1 AND status IN ('pending','running'). Already-terminal hunts (completed / partial / failed / cancelled) are no-ops — the endpoint returns HTTP 409 with the error code not_cancellable.
  2. HTTP 204. On success the server returns no body. The UI’s poll loop picks up the new status on its next 3-second tick and the panel re-renders with status: cancelled.

That’s the whole atomic operation. Notably, the server does not explicitly tear down the in-memory watchHuntTimeout goroutine or cascade into a campaign stop — both are out of scope for the cancel endpoint by design (see below).

What stays vs. what stops accumulating

The status flip is the only direct side effect. Everything else is indirect:

Already happenedOutcome on cancel
Agent replied; result ingested before cancelKept. Visible in the result table.
Agent replied; alert was createdKept. The ioc_alerts row persists.
Agent reply is on the wire when cancel commitsResult row still ingests when it arrives.
Timeout watcher goroutineContinues sleeping until its timer.Now() fires, then runs one UPDATE guarded by WHERE status IN ('pending','running') — the guard makes it a no-op against the already-cancelled row, so the goroutine effectively does nothing.
Global concurrent-hunt slotReleased — the next operator can launch.
Hunt result table in the UIStops polling once the panel re-renders with status: cancelled.

If the hunt was launched via the campaigns subsystem (the offline_campaigns_enabled flag), the campaign’s assignments are not auto-cancelled by stopping the hunt. To stop the campaign too, call the campaign-stop endpoint directly:

Terminal window
curl -X POST -H "Authorization: Bearer <admin-token>" \
-H 'Content-Type: application/json' \
-d '{"discard_inflight":true,"reason":"hunt cancelled"}' \
https://mimir.example.com/api/v1/campaigns/<campaign-id>/stop

The campaign-stop endpoint is what flips campaign assignments in pending, leased, and (with discard_inflight=true) dispatched state to cancelled. The campaign-ingestion path excludes cancelled-status assignments from the responded_hosts / matched_hosts counters via an AND ca.status NOT IN ('cancelled','failed') SQL guard.

Why use it

Two patterns:

  1. Wrong-indicator abort. You pasted the wrong hash into Quick Hunt and the fleet sweep just started. Stop immediately to free the concurrent-hunt slot and stop the UI from continuing to display the active panel.
  2. Sufficient coverage. A fleet hunt is running, you’ve already seen enough matches to act, and the remaining hosts are slow. Stop to release the slot so other operators waiting on the global DefaultMaxConcurrentHunts = 5 cap can launch.

If the hunt is already finished, there’s nothing to stop — the Stop button isn’t even visible. Closing the detail panel with is just a UI dismissal; it does not affect server-side state.

How to use it

  1. Open the Hunts page.
  2. Click into the hunt you want to stop (in the Hunt History table or directly via the active hunt panel if it’s the one you just launched).
  3. In the detail panel header, click the red Stop button. The button shows “Stopping…” briefly, then a toast confirms “Hunt cancelled”. The detail panel re-polls immediately so the status flip is reflected without waiting for the next regular tick.
  4. Confirm in the Hunt History table that the hunt now shows cancelled status.

Who can do it

POST /api/v1/hunts/{id}/cancel is admin-only. The handler calls requireAdmin before touching the database. Non-admin users — even under MIMIR_PER_USER_HUNT_ISOLATION=true, even when they’re the hunt’s owner — receive HTTP 403 forbidden. If your operator role needs to cancel its own hunts in production, ask an admin to grant the admin role or to escalate the cancel via the operator runbook.

A subtle implication: in a shared tenant any admin can cancel any hunt, even one they didn’t launch. If you’re handing out admin roles and you don’t want that, escalate the request — Mimir doesn’t have per-row authorization on cancel today.

Troubleshooting

Stop button is greyed out / shows “Stopping…” forever. The hunt is already in a terminal status — the server saw your request but the WHERE status IN ('pending','running') guard didn’t match. The next poll tick will refresh the panel and the button will disappear. If the spinner is genuinely stuck for more than 10 seconds, the request errored; check the toast for the error message.

HTTP 403 forbidden when I try to cancel. Your session doesn’t have the admin role. The cancel endpoint is admin-only by design. Either re-authenticate as an admin or have one cancel on your behalf.

HTTP 409 not_cancellable. The hunt isn’t in pending or running — most often it just completed naturally between you opening the panel and clicking Stop. Refresh the panel; the status will reflect the actual terminal value.

Cancelled hunt still shows pending alerts in the feed. Two possibilities. (a) The alerts were created from results that arrived BEFORE the cancel — they’re real and stay. (b) The hunt was campaign-backed and you didn’t also stop the campaign — see the curl example above for the campaign-stop endpoint.

Stopped a hunt to free the concurrent-hunt slot but the next launch still returns 429. The cap is global (DefaultMaxConcurrentHunts = 5 by default). Other operators may have running hunts. The cap is admin-tunable via MIMIR_MAX_CONCURRENT_HUNTS in mimir-server.yaml.