# 428 Precondition Required

> The server requires the request to be conditional. Learn when to use 428 Precondition Required to prevent lost updates and race conditions.

Source: https://howhttpworks.com/status-codes/428
Last reviewed: 2026-10-04

> **TL;DR:** Server requires conditional headers (like If-Match) to prevent lost updates in concurrent editing. Get the resource's ETag first, then include it in your update request.

## What is 428 Precondition Required?

A **428 Precondition Required** status code means the server requires the request to include conditional headers before it can be processed. Think of it like a bank requiring you to provide your current account balance before approving a withdrawal—it ensures you're working with up-to-date information and prevents conflicting changes.

This status code is used to prevent the "lost update problem" where multiple clients might modify a resource simultaneously, potentially overwriting each other's changes.

## When Does This Happen?

You'll see a 428 Precondition Required response in these common situations:

**1. Updating Shared Resources**

```text
Multiple users editing the same document
→ Server requires If-Match header
→ Ensures you have the latest version
→ Prevents overwriting others' changes
```

**2. Critical Data Modifications**

```text
Updating financial records
→ Requires If-Unmodified-Since
→ Confirms data hasn't changed
→ Prevents race conditions
```

**3. API Rate Limiting Protection**

```text
High-frequency API endpoint
→ Requires conditional headers
→ Prevents accidental duplicate requests
→ Protects server resources
```

**4. Cache Validation Required**

```text
Critical resource update
→ Server demands If-None-Match
→ Ensures client has current version
→ Prevents stale data updates
```

**5. Distributed System Coordination**

```text
Microservices updating shared state
→ Requires version headers
→ Maintains consistency
→ Prevents conflicts
```

## Example Responses

**Basic 428 Response:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json
Content-Length: 156

{
  "error": "Precondition Required",
  "message": "This request must be conditional. Please include an If-Match or If-Unmodified-Since header.",
  "required_headers": ["If-Match", "If-Unmodified-Since"]
}
```

**With ETag Information:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

{
  "error": "Precondition Required",
  "message": "To update this resource, include an If-Match header with the current ETag",
  "current_etag": "33a64df551425fcc55e4d42a148795d9f25f89d4",
  "example": "If-Match: \"33a64df551425fcc55e4d42a148795d9f25f89d4\""
}
```

**Detailed Guidance:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json
Link: <https://api.example.com/docs/conditional-requests>; rel="help"

{
  "error": "Precondition Required",
  "message": "This endpoint requires conditional request headers to prevent lost updates",
  "details": {
    "reason": "Resource can be modified by multiple clients",
    "required_headers": [
      {
        "name": "If-Match",
        "description": "Include the ETag from your last GET request",
        "example": "If-Match: \"abc123\""
      },
      {
        "name": "If-Unmodified-Since",
        "description": "Include the Last-Modified date from your last GET request",
        "example": "If-Unmodified-Since: Wed, 21 Oct 2025 07:28:00 GMT"
      }
    ]
  },
  "documentation": "https://api.example.com/docs/conditional-requests"
}
```

## Real-World Example

Imagine you're updating a user profile through an API:

**Initial Request (Missing Precondition):**

```http
PUT /api/users/12345 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin"
}
```

**428 Response:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json
Cache-Control: no-cache
Content-Length: 423

{
  "error": "Precondition Required",
  "message": "User profile updates require conditional headers to prevent conflicts",
  "details": {
    "reason": "This resource can be modified by the user and administrators simultaneously",
    "solution": "First, GET the current resource to obtain the ETag, then include it in your PUT request"
  },
  "steps": [
    "1. GET /api/users/12345 to retrieve current data and ETag",
    "2. Include 'If-Match: <etag>' header in your PUT request",
    "3. Retry your PUT request with the If-Match header"
  ],
  "example": {
    "header": "If-Match",
    "value": "\"33a64df551425fcc55e4d42a148795d9f25f89d4\""
  }
}
```

**Corrected Request Flow:**

**Step 1: Get Current Resource**

```http
GET /api/users/12345 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:
HTTP/1.1 200 OK
ETag: "v1.2.3"
Last-Modified: Wed, 18 Jan 2026 10:00:00 GMT

{
  "id": "12345",
  "name": "John Smith",
  "email": "john.smith@example.com",
  "role": "user"
}
```

**Step 2: Update With Precondition**

```http
PUT /api/users/12345 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
If-Match: "v1.2.3"

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin"
}

Response:
HTTP/1.1 200 OK
ETag: "v1.2.4"

{
  "id": "12345",
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "admin",
  "updated_at": "2026-01-18T10:05:00Z"
}
```

## 428 vs Other Conditional Codes

| Code    | Meaning               | Scenario                               | Solution                            |
| ------- | --------------------- | -------------------------------------- | ----------------------------------- |
| **428** | Precondition Required | No conditional header provided         | Add If-Match or If-Unmodified-Since |
| **412** | Precondition Failed   | Conditional header provided but failed | Get latest version and retry        |
| **409** | Conflict              | Request conflicts with current state   | Resolve conflict manually           |
| **304** | Not Modified          | Conditional GET shows no change        | Use cached version                  |

## Important Characteristics

**Prevents Lost Updates:**

```text
User A reads resource (version 1)
User B reads resource (version 1)
User A updates to version 2 ✓
User B updates (would overwrite A's changes)
→ 428 forces B to get version 2 first
```

**Server Policy Decision:**

```http
HTTP/1.1 428 Precondition Required
↑
Server decides which endpoints require conditions
```

**Common Required Headers:**

- `If-Match`: Requires specific ETag
- `If-Unmodified-Since`: Requires resource unchanged since date
- `If-None-Match`: For conditional creation
- Custom version headers

## Common Mistakes

**❌ Not providing guidance**

```http
HTTP/1.1 428 Precondition Required

Precondition Required  ← Unhelpful, what precondition?
```

**❌ Requiring conditions on read-only requests**

```http
GET /api/users/123

HTTP/1.1 428 Precondition Required  ← Bad: GET shouldn't require conditions
```

**❌ Not documenting which headers are needed**

```json
{
  "error": "Precondition Required"
  ← Missing: which headers? which values?
}
```

**✅ Correct usage**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json

{
  "error": "Precondition Required",
  "message": "Include If-Match header with current ETag",
  "required_header": "If-Match",
  "get_etag_from": "GET /api/users/123",
  "current_etag": "\"abc123\""
}
```

## Getting 428 Precondition Required right

**Provide Current ETag:**

```javascript
app.put('/api/resource/:id', async (req, res) => {
  if (!req.headers['if-match']) {
    const resource = await db.getResource(req.params.id)

    return res.status(428).json({
      error: 'Precondition Required',
      message: 'Include If-Match header to update this resource',
      current_etag: resource.etag,
      example: `If-Match: "${resource.etag}"`
    })
  }

  // Process conditional update...
})
```

**Document Required Headers:**

```http
HTTP/1.1 428 Precondition Required
Content-Type: application/json

{
  "error": "Precondition Required",
  "required_headers": {
    "If-Match": {
      "description": "ETag from your last GET request",
      "example": "If-Match: \"v1.2.3\"",
      "obtain_from": "GET /api/resource/:id"
    }
  },
  "documentation": "https://docs.example.com/conditional-requests"
}
```

**Apply to Critical Endpoints Only:**

```javascript
const requiresPrecondition = [
  'PUT /api/users/:id',
  'PATCH /api/documents/:id',
  'DELETE /api/critical-data/:id'
]

// Don't require for less critical operations
const optional = ['POST /api/logs', 'GET /api/*', 'POST /api/analytics']
```

**Consistent Error Format:**

```javascript
function preconditionRequiredError(resourceType, etag) {
  return {
    error: 'Precondition Required',
    message: `${resourceType} updates require conditional headers`,
    required_headers: ['If-Match', 'If-Unmodified-Since'],
    current_etag: etag,
    steps: [
      `1. GET the current ${resourceType}`,
      '2. Note the ETag header in the response',
      '3. Include If-Match: <etag> in your update request'
    ]
  }
}
```

## Implementation Examples

**Express.js Middleware:**

```javascript
const requireConditional = (req, res, next) => {
  const requiresCondition = ['PUT', 'PATCH', 'DELETE'].includes(req.method)
  const hasCondition = req.headers['if-match'] || req.headers['if-unmodified-since']

  if (requiresCondition && !hasCondition) {
    return res.status(428).json({
      error: 'Precondition Required',
      message: 'This operation requires a conditional header',
      required_headers: ['If-Match', 'If-Unmodified-Since'],
      hint: `GET ${req.path} to obtain the current ETag`
    })
  }

  next()
}

app.put('/api/users/:id', requireConditional, async (req, res) => {
  // Handle conditional update...
})
```

**Django:**

```python
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods

@require_http_methods(["PUT"])
def update_user(request, user_id):
    if_match = request.headers.get('If-Match')

    if not if_match:
        return JsonResponse({
            'error': 'Precondition Required',
            'message': 'Include If-Match header with current ETag',
            'required_header': 'If-Match',
            'get_etag_from': f'/api/users/{user_id}'
        }, status=428)

    # Process conditional update
    user = User.objects.get(id=user_id)

    if user.etag != if_match.strip('"'):
        return JsonResponse({
            'error': 'Precondition Failed'
        }, status=412)

    # Update user...
```

**Ruby on Rails:**

```ruby
class UsersController < ApplicationController
  before_action :require_precondition, only: [:update, :destroy]

  def update
    @user = User.find(params[:id])

    if stale?(etag: @user, last_modified: @user.updated_at)
      if @user.update(user_params)
        render json: @user
      else
        render json: @user.errors, status: :unprocessable_entity
      end
    end
  end

  private

  def require_precondition
    unless request.headers['If-Match'] || request.headers['If-Unmodified-Since']
      render json: {
        error: 'Precondition Required',
        message: 'Include If-Match or If-Unmodified-Since header',
        required_headers: ['If-Match', 'If-Unmodified-Since']
      }, status: 428
    end
  end
end
```

## Client-Side Handling

**JavaScript Fetch API:**

```javascript
async function updateUser(userId, updates) {
  // First, get current resource
  const getResponse = await fetch(`/api/users/${userId}`)
  const etag = getResponse.headers.get('ETag')

  // Then update with If-Match
  const putResponse = await fetch(`/api/users/${userId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'If-Match': etag
    },
    body: JSON.stringify(updates)
  })

  if (putResponse.status === 428) {
    throw new Error('Precondition required - this should not happen!')
  }

  if (putResponse.status === 412) {
    // Resource changed, need to refetch
    console.log('Resource was modified, retrying...')
    return updateUser(userId, updates) // Retry
  }

  return putResponse.json()
}
```

## Try It Yourself

Visit our [request builder](https://howhttpworks.com/tools/playground) and see 428 Precondition Required:

1. Set method to **PUT**
2. Set path to **/api/protected-resource**
3. Click **Send request** (without If-Match header)
4. Observe 428 response
5. Add **If-Match** header and retry

## Try it with curl

Send an update to a server that requires a conditional header, with none attached.

```bash
curl -i -X PUT https://api.example.com/documents/1 \
  -H 'Content-Type: application/json' \
  -d '{"title":"New title"}'
```

Example output (illustrative, not captured from a real server):

```http
HTTP/2 428
content-type: application/json

{"error":"precondition_required","hint":"send If-Match"}
```

Fetch the current ETag with `curl -I https://api.example.com/documents/1`, then repeat the `PUT` with `-H 'If-Match: "<etag>"'`.

## Related Status Codes

- [412 Precondition Failed](https://howhttpworks.com/status-codes/412) - Conditional header provided but condition not met
- [409 Conflict](https://howhttpworks.com/status-codes/409) - Request conflicts with current resource state
- [304 Not Modified](https://howhttpworks.com/status-codes/304) - Conditional GET shows resource unchanged
- [200 OK](https://howhttpworks.com/status-codes/200) - Successful conditional update
