Metric · Security

What is failed login rate, and how do you measure it in Metabase?

Failed login rate is failed authentication attempts divided by total attempts, per day and per application. It's two metrics wearing one name: a UX signal (users fumbling passwords after a policy change) and a threat signal (credential stuffing and password sprays) — and the analysis only works if you can tell them apart. Measure it in Metabase from authentication logs synced from Okta or Auth0.

TL;DRfailures ÷ attempts, daily per app with a 7-day moving average. Watch each app against its own baseline, and run the sources-hitting-many-usernames query separately — sprays hide in org-wide averages.

What failed login rate measures

It measures friction and hostility at the front door, in one number — which is why it always needs a second cut before it means anything. Segmented by failure reason, it separates the Monday-morning wrong-password crowd from invalid-username probes and MFA denials. Segmented by source, it exposes sprays that are invisible in the average. Deviations feed detection — a confirmed campaign becomes an input to MTTD — while sustained baseline drift usually points at a UX or policy problem instead.

What data does it need?

  • A login_events table: attempted_at, outcome, failure reason, app_name, username, source_ip.
  • Failure reason codes carried through the sync — they're what separate user error from attack traffic.
  • Source metadata (IP, and ASN or geo if available) for the spray view.
  • A documented rule for what counts as an attempt: raw events or sessionized tries, applied consistently.

SQL patterns

Daily failed-login rate per app, with 7-day moving averagePostgreSQL
WITH daily AS (
  SELECT
    attempted_at::date AS day,
    app_name,
    COUNT(*) AS attempts,
    COUNT(*) FILTER (WHERE outcome = 'failure') AS failures
  FROM login_events
  WHERE attempted_at >= CURRENT_DATE - INTERVAL '90 days'
  GROUP BY 1, 2
)
SELECT
  day,
  app_name,
  attempts,
  ROUND(100.0 * failures / NULLIF(attempts, 0), 2) AS failure_rate_pct,
  ROUND(
    100.0 * SUM(failures) OVER w / NULLIF(SUM(attempts) OVER w, 0), 2
  ) AS failure_rate_7d_avg
FROM daily
WINDOW w AS (
  PARTITION BY app_name
  ORDER BY day
  ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)
ORDER BY app_name, day;
Sources failing across many usernames (spray detection)PostgreSQL
-- Many usernames failing from one source is a spray,
-- whatever the org-wide rate says
SELECT
  source_ip,
  COUNT(*) AS failed_attempts,
  COUNT(DISTINCT username) AS distinct_usernames,
  MIN(attempted_at) AS first_seen,
  MAX(attempted_at) AS last_seen
FROM login_events
WHERE outcome = 'failure'
  AND attempted_at >= now() - INTERVAL '24 hours'
GROUP BY source_ip
HAVING COUNT(DISTINCT username) >= 10
ORDER BY distinct_usernames DESC, failed_attempts DESC;

Pitfalls

Mixing user error with attack traffic.→ Wrong-password-from-a-known-device and invalid-username-from-a-new-ASN are different phenomena with different owners. Segment by failure reason, or the metric is a blur of both.
Reporting rate without volume.→ 50% of 10 attempts is someone's sticky keyboard; 50% of 100,000 is an incident. Keep raw attempt counts on the chart next to the percentage.
Counting every retry as an attempt.→ One user fat-fingering a password five times in a minute isn't five signals. Decide between raw events and sessionized tries, document the choice, and apply it to numerator and denominator alike.
Alerting on the org-wide average.→ A spray across many accounts, or a brute-force against one low-traffic app, barely moves the global rate. Baseline and alert per app, and run source-based spray detection as its own query.

Where this metric applies

Metrics

Dashboards

FAQ

How do I tell user error from attack traffic?
Segment by failure reason — Okta and Auth0 both record why each attempt failed. Wrong password from a known device on a known network is Monday morning; invalid-username failures, or valid passwords failing MFA, from unfamiliar sources are attack signatures. The two populations also move differently: user error tracks the workweek, attack traffic doesn't care what day it is. One blended rate hides both stories.
What failed login rate is normal?
There's no universal baseline — a rarely-used admin console and a daily-use SSO portal have completely different normal ranges, which is exactly why the metric is computed per app with a 7-day moving average. What matters is deviation from each app's own baseline. Also read the rate with volume: 50% of 10 attempts is noise, 50% of 100,000 is an incident, so keep the raw attempts column on the chart.
How does a password spray show up?
Often barely at all in the org-wide rate — that's the trap. A spray tries one or two common passwords against many accounts, staying under per-account lockout thresholds; spread across your whole login volume it can move the average by a rounding error. The signature is one source hitting many usernames: group failures by source_ip and count DISTINCT username. Ten-plus usernames from one source in a day is worth a look regardless of any rate, and confirmed sprays should feed your detection pipeline.
How do you calculate failed login rate?
Divide failed authentication attempts by total attempts per window: failures / attempts, daily and per application, with a 7-day moving average to smooth weekday cycles. Decide what counts as an attempt first — raw events, or sessionized tries where a burst of retries counts once — and apply the same rule to numerator and denominator.
How do you track failed login rate in Metabase?
Sync authentication logs from Okta or Auth0 into a login_events table with outcome, failure reason, app, username, and source IP. Chart the daily per-app rate with its moving average, keep the spray-detection query — sources hitting many usernames — as a standing card, and pair it with MFA adoption rate on an identity and access analytics dashboard: one shows who's being attacked, the other who's protected.