---
title: "Doherty Threshold"
source: "/reference/doherty-threshold"
description: "Interaction feels fluid when feedback arrives quickly enough that users can maintain a continuous task rhythm."
---
# Doherty Threshold

Interaction feels fluid when feedback arrives quickly enough that users can maintain a continuous task rhythm.



### Principle

Interaction feels fluid when feedback arrives quickly enough that users can maintain a continuous task rhythm.

### Why it matters

The historical Doherty Threshold is commonly associated with roughly 400 milliseconds, but modern product performance should be evaluated across several feedback horizons rather than one universal number. Immediate acknowledgement, responsive input, progressive rendering, optimistic updates, and honest progress indicators can preserve continuity even when backend work takes longer.

### Deeper design interpretation

Responsiveness is experienced as a sequence, not as one number. Users act, expect acknowledgement, observe progress, and decide whether to continue. A system can take several seconds to complete a task and still feel controlled when it immediately acknowledges input and accurately communicates what is happening.

The historic threshold is useful as context, yet modern interfaces should distinguish several latency layers: input response, visual acknowledgement, data availability, durable completion, and background work. Treating all of them as one metric leads to misleading optimizations.

Layr should focus on continuity and truthfulness. A fast-looking interface that declares success before persistence is complete can create more serious problems than a slower interface with honest state.

### What the agent should reason about

Measure or inspect each stage of the action-feedback loop. Determine when the user receives acknowledgement, when useful partial results are available, when server state becomes durable, and whether the user can safely repeat, cancel, or leave the task.

For long-running jobs, the ideal UI often shifts from “wait here” to “this is now a durable process you can revisit.”

### Common failure patterns

- Buttons remain visually unchanged while requests are in flight.
- Duplicate jobs are created because users retry an apparently unresponsive action.
- Blocking spinners hide partial content that could already be used.
- Optimistic success is shown for actions that may fail irreversibly.
- Long-running operations have no durable job page, stage, or retry state.

### Audit questions

- How quickly does the interface acknowledge the action?
- When is the operation actually durable?
- Can the user safely navigate away or retry?
- Is progress truthful enough to support the next decision?

### Example agent finding

> **High — Starting a migration has an eight-second silent period.** The CTA remains enabled and unchanged until the server returns, allowing duplicate starts. Disable or transition the button immediately, create an idempotent job, and expose the durable job state while processing continues.

### Agent directive

> Prioritize immediate acknowledgement and continuous feedback. Measure real latency and distinguish input response, visual feedback, data completion, and background work.

### Apply this when

- Search, filters, editors, navigation, save actions, command palettes, drag/drop, AI generation, imports, and long-running tasks.

### Observable signals

- A click produces no visible response while work is in progress.
- Repeated user input is accepted because the first action appears to have failed.
- A blocking spinner hides partial results that could be useful earlier.
- The UI reports completion before server state is actually durable.
- Long-running operations provide no progress, stage, cancellation, or recovery information.

### Recommended actions

- Acknowledge input immediately through pressed, pending, skeleton, progress, or status states.
- Render useful partial content when safe.
- Use optimistic updates only when rollback is reliable and the consequence is low enough.
- For long jobs, expose durable job state and allow users to leave and return.
- Measure latency before prescribing animation as a performance fix.

### Do not over-apply

Do not fake speed by hiding failure or claiming success early. Performance perception must remain truthful about durable state.

### Example

**Weak:** A 'Start migration' button remains unchanged for eight seconds, inviting repeated clicks and duplicate jobs.

**Stronger:** The action immediately enters a pending state, creates one durable job, reports current stage, and lets the user navigate away safely.

### Evaluation notes

- **Default severity:** High for duplicate actions, data loss, or core interaction lag; medium for perceived slowness.
- **Typical confidence:** High with performance traces; medium from UI behavior alone.
- **Primary evidence:** Performance metrics, network timing, pending states, duplicate events, job model.
- **Related rules:** Flow, Fitts's Law, Peak-End Rule, Pareto Principle

### References

- [Laws of UX — Doherty Threshold](https://lawsofux.com/doherty-threshold/)

## Related rules

- [Flow](/__layr/pages/reference/flow.md)
- [Fitts's Law](/__layr/pages/reference/fitts-law.md)
- [Peak-End Rule](/__layr/pages/reference/peak-end-rule.md)
- [Pareto Principle](/__layr/pages/reference/pareto-principle.md)

## Agent guidance

Review criteria, the finding contract a report must use, and worked examples.

Established principle; Layr interpretation and agent guidance. Category: Interaction, Performance & Error Tolerance.

## Retrieve
- Canonical page: https://layr.design/reference/doherty-threshold
- Markdown for agents: https://layr.design/__layr/pages/reference/doherty-threshold.md
- Reference map: https://layr.design/llms.txt
- Route index: https://layr.design/__layr/routes.toon

## Rule object
```yaml
rule:
  id: "doherty-threshold"
  title: "Doherty Threshold"
  category: "Interaction, Performance & Error Tolerance"
  origin: established
  canonical_url: "https://layr.design/reference/doherty-threshold"
  principle: "Interaction feels fluid when feedback arrives quickly enough that users can maintain a continuous task rhythm."
  directive: "Prioritize immediate acknowledgement and continuous feedback. Measure real latency and distinguish input response, visual feedback, data completion, and background work."
  applies_when:
    - "Search, filters, editors, navigation, save actions, command palettes, drag/drop, AI generation, imports, and long-running tasks."
  observable_signals:
    - "A click produces no visible response while work is in progress."
    - "Repeated user input is accepted because the first action appears to have failed."
    - "A blocking spinner hides partial results that could be useful earlier."
    - "The UI reports completion before server state is actually durable."
    - "Long-running operations provide no progress, stage, cancellation, or recovery information."
  audit_questions:
    - "How quickly does the interface acknowledge the action?"
    - "When is the operation actually durable?"
    - "Can the user safely navigate away or retry?"
    - "Is progress truthful enough to support the next decision?"
  recommended_actions:
    - "Acknowledge input immediately through pressed, pending, skeleton, progress, or status states."
    - "Render useful partial content when safe."
    - "Use optimistic updates only when rollback is reliable and the consequence is low enough."
    - "For long jobs, expose durable job state and allow users to leave and return."
    - "Measure latency before prescribing animation as a performance fix."
  avoid_when:
    - "Do not fake speed by hiding failure or claiming success early. Performance perception must remain truthful about durable state."
  common_failure_patterns:
    - "Buttons remain visually unchanged while requests are in flight."
    - "Duplicate jobs are created because users retry an apparently unresponsive action."
    - "Blocking spinners hide partial content that could already be used."
    - "Optimistic success is shown for actions that may fail irreversibly."
    - "Long-running operations have no durable job page, stage, or retry state."
  evidence_types:
    - "Performance metrics"
    - "network timing"
    - "pending states"
    - "duplicate events"
    - "job model"
  default_severity: "High for duplicate actions, data loss, or core interaction lag; medium for perceived slowness."
  typical_confidence: "High with performance traces; medium from UI behavior alone."
  related_rules:
    - { id: "flow", title: "Flow", url: "https://layr.design/reference/flow" }
    - { id: "fitts-law", title: "Fitts's Law", url: "https://layr.design/reference/fitts-law" }
    - { id: "peak-end-rule", title: "Peak-End Rule", url: "https://layr.design/reference/peak-end-rule" }
    - { id: "pareto-principle", title: "Pareto Principle", url: "https://layr.design/reference/pareto-principle" }
```

## What to reason about
Measure or inspect each stage of the action-feedback loop. Determine when the user receives acknowledgement, when useful partial results are available, when server state becomes durable, and whether the user can safely repeat, cancel, or leave the task. For long-running jobs, the ideal UI often shifts from “wait here” to “this is now a durable process you can revisit.”

## Routing
Route a review here when evidence is measurable in interaction behavior: latency, target geometry, validation, parsing, task completion, duplicate actions, failed input, or recovery. Prefer instrumented evidence over aesthetic judgment.

## Review procedure
For every finding:
1. Identify the exact component, state, route, or interaction.
2. State the observable evidence.
3. Name the principle that applies.
4. Explain the user consequence in the current context.
5. Estimate severity from the consequence, frequency, and reversibility.
6. Recommend the smallest change that addresses the issue while preserving the product's existing design system.
7. Lower confidence when intent, frequency, audience, device, or product constraints must be inferred.
8. Do not produce generic advice such as "improve hierarchy" or "make this clearer" without specifying what is wrong and why.

## Finding contract — emit exactly this shape
```yaml
finding:
  rule_id: string
  location: string
  evidence:
    - observable fact
  impact: string
  severity: low | medium | high | critical
  confidence: low | medium | high
  recommendation:
    change: string
    rationale: string
    preserve:
      - existing design-system behavior
      - valid product constraints
```

## Example finding for this rule
High — Starting a migration has an eight-second silent period. The CTA remains enabled and unchanged until the server returns, allowing duplicate starts. Disable or transition the button immediately, create an idempotent job, and expose the durable job state while processing continues.

## Code reference

The same principle as markup. Compare the structure, not the class names.

```html
<!-- weak -->
<div class="example-ui example-ui-weak">
  <div class="scene-chrome">
    <span class="scene-chrome-app">Migration</span>
    <span class="scene-chrome-view">Move records</span>
  </div>
  <div class="scene-body">
    <div class="scene-panel">
      <span class="scene-panel-title">Move 1,284 records</span>
      <div class="scene-row">
        <span class="scene-action scene-action-primary">Start migration</span>
        <span class="scene-meta">clicked 3× · 8.2 s with no change</span>
      </div>
      <div class="scene-jobs">
        <span class="scene-meta">Jobs created</span>
        <div class="scene-row">
          <span class="scene-key">M-104</span>
          <span class="scene-tag scene-tag-warning">Duplicate</span>
        </div>
        <div class="scene-row">
          <span class="scene-key">M-105</span>
          <span class="scene-tag scene-tag-warning">Duplicate</span>
        </div>
        <div class="scene-row">
          <span class="scene-key">M-106</span>
          <span class="scene-tag scene-tag-warning">Duplicate</span>
        </div>
      </div>
      <span class="scene-note">No pending state · three jobs for one intent</span>
    </div>
  </div>
</div>
```

```html
<!-- stronger -->
<div class="example-ui example-ui-strong">
  <div class="scene-chrome">
    <span class="scene-chrome-app">Migration</span>
    <span class="scene-chrome-view">Move records</span>
  </div>
  <div class="scene-body">
    <div class="scene-panel">
      <span class="scene-panel-title">Move 1,284 records</span>
      <div class="scene-row">
        <span class="scene-action scene-action-disabled">Starting…</span>
        <span class="scene-meta">Acknowledged in 90 ms</span>
      </div>
      <span class="scene-progress scene-progress-indeterminate">
        <span class="scene-progress-fill"></span>
      </span>
      <div class="scene-row">
        <span class="scene-key">M-104</span>
        <span class="scene-tag scene-tag-accent">Stage 2 of 4 · mapping</span>
      </div>
      <div class="scene-row scene-row-muted">
        <span class="scene-meta">412 of 1,284 records</span>
        <span class="scene-meta">Safe to leave this page</span>
      </div>
      <span class="scene-note">One durable job · honest stage · recoverable</span>
    </div>
  </div>
</div>
```

```css
/* declarations these studies depend on */
.example-ui { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; min-height: 17rem; overflow: hidden; border: var(--border-thin) solid var(--color-border-strong); background: var(--color-background); color: var(--color-text); font-size: var(--text-xs); line-height: var(--leading-snug); }
.example-ui .scene-chrome { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); padding: var(--space-2) var(--space-3); border-bottom: var(--border-thin) solid var(--color-border-strong); background: var(--color-surface); }
.example-ui .scene-chrome-app { font-size: var(--text-2xs); font-weight: 600; letter-spacing: var(--tracking-label); text-transform: uppercase; }
.example-ui .scene-chrome-view { color: var(--color-text-tertiary); font-size: var(--text-2xs); }
.example-ui .scene-body { display: grid; align-content: start; gap: var(--space-3); min-width: 0; padding: var(--space-3); }
.example-ui .scene-body:has(> .scene-panel:only-child) { align-content: stretch; }
.example-ui .scene-panel { display: grid; align-content: start; justify-items: start; gap: var(--space-2); min-width: 0; padding: var(--space-3); border: var(--border-thin) solid var(--color-border-strong); background: var(--color-surface-raised); }
.example-ui .scene-panel-title { font-size: var(--text-sm); font-weight: 600; letter-spacing: var(--tracking-heading); }
.example-ui .scene-row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-1) var(--space-2); width: 100%; min-width: 0; padding-block: var(--space-1); }
.example-ui .scene-panel > .scene-row + .scene-row { border-top: var(--border-thin) solid var(--color-border-subtle); }
.example-ui .scene-row-muted { color: var(--color-text-tertiary); }
.example-ui .scene-meta { color: var(--color-text-tertiary); font-size: var(--text-2xs); }
.example-ui .scene-key { font-family: var(--font-mono); font-size: var(--text-2xs); }
.example-ui .scene-note { width: 100%; padding-top: var(--space-2); border-top: var(--border-thin) solid var(--color-border-subtle); color: var(--color-text-tertiary); font-family: var(--font-mono); font-size: var(--text-2xs); line-height: var(--leading-snug); }
.example-ui .scene-action { display: inline-flex; align-items: center; justify-content: center; min-height: var(--space-8); padding-inline: var(--space-3); border: var(--border-thin) solid var(--color-border-strong); border-radius: var(--radius-xs); background: var(--color-surface-raised); color: var(--color-text); font-size: var(--text-xs); font-weight: 550; white-space: nowrap; }
.example-ui .scene-action-primary { border-color: var(--color-primary-bg); background: var(--color-primary-bg); color: var(--color-primary-text); }
.example-ui .scene-action-disabled { border-color: var(--color-border-subtle); background: var(--color-surface); color: var(--color-text-tertiary); }
.example-ui .scene-tag { display: inline-block; padding: 0 var(--space-2); border: var(--border-thin) solid var(--color-border-strong); color: var(--color-text-secondary); font-size: var(--text-2xs); line-height: 1.6; white-space: nowrap; }
.example-ui .scene-tag-accent { border-color: var(--color-action); color: var(--color-accent-text); }
.example-ui .scene-tag-warning { border-color: var(--color-caution); color: var(--color-caution); }
.example-ui .scene-progress { display: block; width: 100%; height: var(--space-2); background: var(--diagram-fill); }
.example-ui .scene-progress-fill { display: block; height: 100%; background: var(--color-action); }
.example-ui .scene-progress-indeterminate .scene-progress-fill { width: 38%; background: repeating-linear-gradient(90deg, var(--color-action) 0 6px, transparent 6px 10px); }
.example-ui .scene-jobs { display: grid; justify-items: start; gap: var(--space-1); width: 100%; padding-top: var(--space-2); border-top: var(--border-thin) solid var(--color-border-subtle); }
.example-ui { min-height: 15rem; }

/* resolved values */
:root {
  --border-thin: 1px;
  --font-mono: "Geist Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  --leading-snug: 1.35;
  --radius-xs: 2px;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-8: 2rem;
  --text-2xs: 0.6875rem;
  --text-sm: 0.875rem;
  --text-xs: 0.75rem;
  --tracking-heading: -0.025em;
  --tracking-label: 0.06em;
}
```

## Weak vs stronger
- Weak: A 'Start migration' button remains unchanged for eight seconds, inviting repeated clicks and duplicate jobs.
- Stronger: The action immediately enters a pending state, creates one durable job, reports current stage, and lets the user navigate away safely.

## Reporting rules
- Report only what the interface shows as observable evidence; phrase psychological or motivational effects as possible consequences.
- Returning no finding is a valid result when evidence is insufficient.
