# How Ghost Implements Spam Prevention in Its API: Express-Brute and Knex Store Architecture

> Learn how Ghost prevents API spam with express-brute and its Knex store architecture. Discover how request counters and HTTP 429 errors block unwanted traffic efficiently.

- Repository: [Ghost/Ghost](https://github.com/TryGhost/Ghost)
- Tags: internals
- Published: 2026-05-18

---

**Ghost prevents API spam using a centralized rate-limiting layer built on `express-brute` and `brute-knex` that stores request counters in a Knex-backed database table and returns HTTP 429 errors with customizable retry delays.**

Ghost protects its public authentication, content, and management endpoints using a sophisticated rate-limiting system implemented in [`ghost/core/core/server/web/shared/middleware/api/spam-prevention.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/web/shared/middleware/api/spam-prevention.js). The architecture leverages **Express-Brute** with a **Knex-backed store** to persist request counters across server restarts, enabling precise throttling for high-risk operations like login attempts and password resets.

## Core Architecture: Database-Backed Rate Limiting

The spam prevention system relies on `express-brute` middleware paired with `brute-knex` to store rate-limiting state in the database rather than memory. This ensures that request counters survive server restarts and work correctly in multi-instance deployments.

### The Knex Store Implementation

At initialization, the system creates a single `ExpressBrute` instance backed by a Knex store named `brute`. This store writes to a dedicated `brute` table in the database, tracking request counts and timestamps per client identifier. By using database persistence instead of in-memory storage, Ghost ensures that rate limits remain consistent across load-balanced environments.

### Configuration-Driven Limits

Each limiter reads its constraints from `config.get('spam')`, with sensible defaults defined directly in the spam-prevention module. The configuration object supports standard Express-Brute parameters including:

- `freeRetries`: Number of allowed attempts before throttling begins
- `minWait` and `maxWait`: Minimum and maximum wait times between retries
- `lifetime`: Duration that request records persist in the database

## Endpoint-Specific Rate Limiting Rules

Ghost defines separate `ExpressBrute` instances for distinct attack vectors, each with tailored limits and key generation strategies.

### Global IP-Based Protection

The **globalBlock** limiter protects against brute-force attacks on any IP-based endpoint, allowing 50 attempts per hour before blocking for one hour. For sensitive operations like password reset token generation, the **globalReset** limiter enforces stricter limits of 5 attempts per hour with a similar one-hour block duration.

### User-Specific Authentication Throttling

The **userLogin** limiter implements a Fibonacci back-off strategy tied to the combination of user identity and IP address. It permits 5 attempts per user-IP pair, with exponentially increasing delays between subsequent requests. This prevents credential stuffing while minimizing impact on legitimate users who might mistype passwords.

### Member Authentication and Enumeration Protection

For member sign-ins, the **membersAuth** limiter restricts attempts to 5 per email address per hour. The **membersAuthEnumeration** limiter uses similar constraints but resets its counter on successful authentication, preventing attackers from testing email validity without triggering rate limits on active accounts.

### One-Time Code and Specialized Limiters

Ghost includes dedicated limiters for **otcVerification** (one-time code verification) and **otcVerificationEnumeration**, both allowing 5 attempts per code or IP address. Additional limiters protect specific features: **privateBlog** (10 attempts per hour for private site access), **contentApiKey** (configurable per-key limits using an in-memory store), **webmentionsBlock** (prevents mention spam), and **emailPreviewBlock** (limits test emails to 10 per hour).

## Middleware Integration with Express Routes

Individual rate limiters are exposed through [`ghost/core/core/server/web/shared/middleware/brute.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/web/shared/middleware/brute.js), which translates each spam-prevention rule into standard Express middleware. This abstraction allows routes to apply rate limiting without managing Express-Brute configuration directly.

For example, attaching member authentication protection to a route:

```javascript
// In the members service router
const brute = require('../../../../web/shared/middleware/brute');

router.post('/signin', brute.membersAuth, async (req, res, next) => {
    // Normal sign-in logic executes only if rate limit permits
});

```

The brute middleware factory wraps the underlying `ExpressBrute` instances with `getMiddleware()` calls, passing route-specific options like `ignoreIP` and custom key generation functions.

## Error Handling and Client Feedback

When a request exceeds its allocated rate limit, the `failCallback` defined in [`spam-prevention.js`](https://github.com/TryGhost/Ghost/blob/main/spam-prevention.js) creates a `TooManyRequestsError` with HTTP status 429. The error response includes a human-readable message indicating the remaining wait time, constructed using the `minWait` and `maxWait` configuration values.

The system also centralizes store error handling, ensuring that database connection failures or Knex errors are logged and transformed into appropriate HTTP errors rather than crashing the request pipeline.

## Practical Configuration and Usage Examples

### Attaching Limiters to Routes

Routes import the brute middleware and apply specific limiters as middleware functions:

```javascript
const brute = require('ghost/core/core/server/web/shared/middleware/brute');

// Protect password reset endpoint
router.post('/authentication/passwordreset', brute.globalReset, controller);

```

### Manual Limiter Management

For testing or administrative purposes, limiters expose a `reset()` method that clears counters from both memory and the database:

```javascript
const spamPrevention = require('ghost/core/core/server/web/shared/middleware/api/spam-prevention');

// Clear all counters for user login attempts
await spamPrevention.userLogin().reset();

```

### Customizing Rate Limits

Administrators can override default limits via [`config.production.json`](https://github.com/TryGhost/Ghost/blob/main/config.production.json) or environment variables:

```json
{
  "spam": {
    "user_login": {
      "freeRetries": 5,
      "minWait": 60,
      "maxWait": 3600,
      "lifetime": 86400
    },
    "global_block": {
      "freeRetries": 50,
      "minWait": 3600000,
      "lifetime": 3600
    }
  }
}

```

Changes to the `config/spam` section take effect without server restart when invoking the exported `reset()` method on the spam prevention module.

## Summary

- Ghost implements API spam prevention using `express-brute` middleware with a `brute-knex` store that persists request data to a database table named `brute`
- Rate limits are configurable via `config.get('spam')` with defaults defined in [`spam-prevention.js`](https://github.com/TryGhost/Ghost/blob/main/spam-prevention.js)
- Separate limiters protect specific attack vectors including global IP blocks, user logins, member authentication, and one-time code verification
- Exceeded limits return HTTP 429 `TooManyRequestsError` with human-readable retry timing
- The `reset()` method allows clearing counters and reloading configuration without server restart

## Frequently Asked Questions

### What database table does Ghost use for rate limiting storage?

Ghost stores rate-limiting counters in a table named `brute`, accessed through the `brute-knex` store adapter. This ensures persistence across server restarts and compatibility with multi-instance deployments, unlike in-memory storage alternatives.

### How does Ghost handle requests that exceed rate limits?

When a client exceeds its allocated attempts, the `failCallback` in [`spam-prevention.js`](https://github.com/TryGhost/Ghost/blob/main/spam-prevention.js) generates a `TooManyRequestsError` with HTTP status 429. The response includes a human-readable message indicating how long the client must wait before retrying, calculated from the configured `minWait` and `maxWait` values.

### Can rate limiting configuration be changed without restarting Ghost?

Yes. The spam prevention module exports a `reset()` method that clears all in-memory `ExpressBrute` instances and reloads configuration from `config.get('spam')`. This allows runtime updates to rate limits by calling the reset method after modifying configuration files.

### What is the difference between the globalBlock and userLogin limiters?

The **globalBlock** limiter tracks attempts by IP address alone (50 attempts per hour), protecting against general brute-force attacks. The **userLogin** limiter tracks attempts by the combination of user identity and IP address using Fibonacci back-off, specifically preventing credential stuffing while reducing false positives for legitimate users who mistype passwords.