# How Logto's Sentinel System Handles Rate Limiting and Brute-Force Protection

> Discover how Logto's sentinel system uses rate limiting and brute-force protection to secure your applications. Learn about its database-backed, policy-driven approach.

- Repository: [Logto/logto](https://github.com/logto-io/logto)
- Tags: internals
- Published: 2026-07-05

---

**Logto's sentinel system for rate limiting is a database-backed, policy-driven subsystem that tracks failed authentication attempts in a `sentinel_activities` table, blocks further attempts after a configurable threshold, and automatically expires the block after a set duration to protect against brute-force attacks.**

Logto implements a robust **sentinel system for rate limiting** to safeguard authentication flows from brute-force attacks and abuse. This subsystem operates as a tenant-wide policy engine that monitors sign-in activities, enforces lockout mechanisms, and prevents spam through integrated message rate limiting. According to the logto-io/logto source code, the implementation centers around the `BasicSentinel` class and a dedicated `MessageRateGuard` for comprehensive protection.

## Core Components of the Sentinel System

### Sentinel Policy Configuration

The sentinel behavior is governed by a **Sentinel Policy** stored in the tenant's sign-in experience configuration. Default values are defined in [`packages/schemas/src/consts/sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/consts/sentinel.ts), specifying `maxAttempts: 100` and `lockoutDuration: 60` minutes. Administrators can override these defaults per-tenant through the sign-in experience settings.

### Activity Recording and Storage

Every authentication-related action generates an **activity** record stored in the `sentinel_activities` table. The activity payload follows the `ActivityReport` type defined in [`packages/schemas/src/types/sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/types/sentinel.ts), capturing the target hash, action type, result (success or failure), and timestamp. This persistent storage enables the system to track failed attempts across distributed instances.

## How BasicSentinel Implements Rate Limiting

### The Decision Workflow

The `BasicSentinel` class in [`packages/core/src/sentinel/basic-sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/sentinel/basic-sentinel.ts) serves as the default implementation. Its workflow consists of two primary methods:

1. **assertAction** – Validates that the incoming action belongs to either the pooled or isolated action categories.
2. **decide** – Determines whether to allow or block the request by checking existing blocks and counting recent failed attempts.

The `decide` method queries the `sentinel_activities` table to check if the target is already blocked via `isBlocked`. If not blocked, it counts failed attempts from the last hour for the same pooled actions. When the count reaches `maxAttempts`, it returns a `Blocked` decision with an expiration timestamp calculated as `now + lockoutDuration`. Otherwise, it returns `Allowed`.

```typescript
import BasicSentinel from '#src/sentinel/basic-sentinel.js';
import { type ActivityReport } from '@logto/schemas';

// Example: reporting a password-verification activity
async function verifyPassword(
  sentinel: BasicSentinel,
  targetHash: string,
  passwordCorrect: boolean
) {
  const activity: ActivityReport = {
    targetType: SentinelActivityTargetType.User,
    targetHash,
    action: SentinelActivityAction.Password,
    actionResult: passwordCorrect
      ? SentinelActionResult.Success
      : SentinelActionResult.Failed,
    payload: {}
  };

  // Returns [decision, expiresAt]
  const [decision, expiresAt] = await sentinel.reportActivity(activity);

  if (decision === SentinelDecision.Blocked) {
    throw new Error(`Account locked until ${new Date(expiresAt).toISOString()}`);
  }

  // Continue with normal login flow...
}

```

### Action Isolation for MFA Protection

Multi-factor authentication actions receive special treatment through **action isolation**. Unlike password attempts that pool together, MFA actions (TOTP, WebAuthn, backup codes) use separate activity pools via the `isolatedActionArrays` map and `getActionGuard` helper. This prevents a lockout on one authentication factor from affecting others, maintaining security without compromising availability.

## Message Rate Limiting with MessageRateGuard

Beyond login attempt throttling, Logto protects against outbound message spam through `MessageRateGuard` located in [`packages/core/src/sentinel/message-rate-guard.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/sentinel/message-rate-guard.ts). This guard enforces `defaultMessageRateLimitPolicy` to limit verification codes and invitation emails per recipient. Unlike the main sentinel policy, message rate limits are not tenant-configurable. The `withMessageRateGuard` wrapper checks send counts before transmission and records successful sends to prevent abuse.

```typescript
import { withMessageRateGuard } from '#src/sentinel/message-rate-guard.js';
import { sendVerificationEmail } from './email';

// Guard a verification-code email send
async function sendCode(email: string, guard: MessageRateGuard) {
  await withMessageRateGuard(guard, {
    action: SentinelActivityAction.VerificationCode,
    recipient: email,
    onRateLimited: () => console.warn('Message rate limit exceeded')
  }, async () => {
    await sendVerificationEmail(email);
  });
}

```

## Integration with Sign-In Experience

The sentinel system integrates with Logto's management APIs through the sign-in experience route at [`packages/core/src/routes/sign-in-experience/index.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/routes/sign-in-experience/index.ts). This route reads the `sentinelPolicy` configuration and injects it into `BasicSentinel` instances. The admin console parses these policies for display using the parser in [`packages/console/src/pages/SignInExperience/PageContent/utils/parser.ts`](https://github.com/logto-io/logto/blob/main/packages/console/src/pages/SignInExperience/PageContent/utils/parser.ts), allowing administrators to configure protection levels through the UI.

## Summary

- The sentinel system uses a tenant-configurable policy with default values of 100 max attempts and 60-minute lockout durations defined in [`packages/schemas/src/consts/sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/consts/sentinel.ts).
- `BasicSentinel` in [`packages/core/src/sentinel/basic-sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/sentinel/basic-sentinel.ts) tracks failed attempts in the `sentinel_activities` table and issues `Blocked` decisions when thresholds are exceeded.
- MFA actions are isolated into separate pools to prevent cascading lockouts across authentication factors.
- `MessageRateGuard` provides additional protection for outbound communications using non-configurable rate limits.
- Integration occurs through the sign-in experience routes and admin console parsers for seamless policy management.

## Frequently Asked Questions

### What triggers a block in Logto's sentinel system?

A block triggers when the number of failed attempts for a specific target (user/email/phone hash) reaches the `maxAttempts` threshold (default 100) within a one-hour window. The `decide` method in `BasicSentinel` counts recent failed activities from the `sentinel_activities` table and returns a `Blocked` decision with an expiration timestamp set to the current time plus the `lockoutDuration`.

### How does the sentinel system handle MFA attempts differently from password attempts?

The sentinel system isolates MFA actions (TOTP, WebAuthn, backup codes) into separate activity pools using the `isolatedActionArrays` map in `BasicSentinel`. This ensures that excessive failed attempts on one MFA factor do not lock out other factors or the primary password authentication, maintaining layered security without single points of failure.

### Can administrators customize the rate limiting thresholds?

Yes, administrators can override the default sentinel policy values stored in the sign-in experience configuration. While the defaults (`maxAttempts: 100`, `lockoutDuration: 60min`) are defined in [`packages/schemas/src/consts/sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/consts/sentinel.ts), the system reads custom policies from the tenant configuration in [`packages/core/src/routes/sign-in-experience/index.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/routes/sign-in-experience/index.ts). However, the `MessageRateGuard` limits for outbound messages are not tenant-configurable.

### Where does the sentinel system store activity records?

Activity records persist in the `sentinel_activities` database table, which tracks every sign-in-related action including passwords, verification codes, and MFA attempts. The schema stores target hashes, action types, results, and timestamps, enabling `BasicSentinel` to query historical attempts when making block decisions through SQL logic in the `isBlocked` and `decide` methods.