# How the OpenCTI Indicator Decay Manager Works: A Technical Deep Dive

> Understand how the OpenCTI indicator decay manager automatically ages indicators. Learn about scheduled jobs, score recomputation, and validity updates for stale indicators.

- Repository: [OpenCTI Platform/opencti](https://github.com/opencti-platform/opencti)
- Tags: deep-dive
- Published: 2026-02-19

---

**The OpenCTI indicator decay manager automatically ages Indicators by running a scheduled background job that selects stale indicators, recomputes their scores based on Decay Rules, and updates their validity status.**

OpenCTI, an open-source cyber threat intelligence platform, implements automatic indicator lifecycle management through the **indicator decay manager**. This system ensures that Indicators (IP addresses, file hashes, domains, etc.) automatically lose relevance over time according to configurable decay rules, reducing false positives from outdated threat intelligence.

## What Is the Indicator Decay Manager?

The indicator decay manager is a background service registered with OpenCTI's generic manager framework. It operates on a scheduled interval to process batches of Indicators whose `decay_next_reaction_date` has passed. For each selected Indicator, the manager:

1. Retrieves the associated **Decay Rule** (containing score points, revoke thresholds, and reaction dates)
2. Calculates the next stable score based on the decay curve
3. Determines if the Indicator should be revoked
4. Updates the Indicator's score, history, and next reaction timestamp

The implementation resides primarily in [`opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts) with core logic delegated to [`src/modules/indicator/indicator-domain.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/src/modules/indicator/indicator-domain.ts).

## Configuration and Initialization

### Manager Settings

The indicator decay manager is controlled through configuration keys loaded at startup in [`indicatorDecayManager.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/indicatorDecayManager.ts):

| Config Key | Default | Purpose |
|------------|---------|---------|
| `indicator_decay_manager:enabled` | `true` | Master switch to enable/disable the manager |
| `indicator_decay_manager:lock_key` | `indicator_decay_manager_lock` | Distributed lock key preventing parallel execution across cluster nodes |
| `indicator_decay_manager:interval` | `60000` (1 minute) | Milliseconds between scheduler invocations |
| `indicator_decay_manager:batch_size` | `10000` | Maximum indicators processed per execution cycle |

```typescript
const INDICATOR_DECAY_MANAGER_ENABLED = booleanConf('indicator_decay_manager:enabled', true);
const INDICATOR_DECAY_MANAGER_KEY = conf.get('indicator_decay_manager:lock_key') || 'indicator_decay_manager_lock';
const SCHEDULE_TIME = conf.get('indicator_decay_manager:interval') || 60000;
const BATCH_SIZE = conf.get('indicator_decay_manager:batch_size') || 10000;

```

### Registration with the Manager Framework

The manager registers itself using the `registerManager` function, providing a cron-like handler that the framework invokes on the defined interval:

```typescript
const INDICATOR_DECAY_MANAGER_DEFINITION: ManagerDefinition = {
  id: 'INDICATOR_DECAY_MANAGER',
  label: 'Indicator decay manager',
  executionContext: 'indicator_decay_manager',
  cronSchedulerHandler: {
    handler: indicatorDecayHandler,
    interval: SCHEDULE_TIME,
    lockKey: INDICATOR_DECAY_MANAGER_KEY,
  },
  enabledByConfig: INDICATOR_DECAY_MANAGER_ENABLED,
  enabledToStart() { return this.enabledByConfig; },
  enabled() { return this.enabledByConfig; },
};

registerManager(INDICATOR_DECAY_MANAGER_DEFINITION);

```

## How the Decay Process Works

### Step 1: Selecting Stale Indicators

The `indicatorDecayHandler` function creates an execution context and queries for indicators requiring decay using `findIndicatorsForDecay` from [`src/modules/indicator/indicator-domain.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/src/modules/indicator/indicator-domain.ts):

```typescript
const context = executionContext('indicator_decay_manager');
const indicatorsToUpdate = await findIndicatorsForDecay(context, DECAY_MANAGER_USER, BATCH_SIZE);

```

The selector queries the database with specific filters:

```typescript
export const findIndicatorsForDecay = (context, user, maxSize) => {
  const filters = {
    orderBy: 'decay_next_reaction_date',
    orderMode: OrderingMode.Asc,
    mode: FilterMode.And,
    filters: [
      { key: ['decay_next_reaction_date'], values: [prepareDate()], operator: FilterOperator.Lt },
      { key: ['revoked'], values: ['false'] },
    ],
    filterGroups: [],
  };
  const args = { filters, maxSize };
  return fullEntitiesList(context, user, [ENTITY_TYPE_INDICATOR], args);
};

```

This query retrieves up to `BATCH_SIZE` indicators where:
- `decay_next_reaction_date` is less than the current time (stale)
- `revoked` is `false` (not already revoked)
- Results are ordered by reaction date (oldest first)

### Step 2: Computing the Next Decay Step

For each selected indicator, the handler invokes `updateIndicatorDecayScore`, which delegates to `computeIndicatorDecayPatch` in [`indicator-domain.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/indicator-domain.ts). This function calculates the new score based on the associated Decay Rule's `decay_points` array:

```typescript
const newStableScore = model.decay_points.find(p => (p || indicator.x_opencti_score) < indicator.x_opencti_score) || model.decay_revoke_score;
if (newStableScore) {
  const newDecayHistoryPoint = { updated_at: new Date(), score: newStableScore, updated_by: user.id };
  const decayHistory = computeIndicatorDecayHistory([...indicator.decay_history ?? []], newDecayHistoryPoint);
  patch = { x_opencti_score: newStableScore, decay_history: decayHistory };
  
  if (newStableScore <= model.decay_revoke_score) {
    patch = { ...patch, revoked: true, x_opencti_detection: false };
  } else {
    const nextScoreReactionDate = computeNextScoreReactionDate(indicator.decay_base_score, newStableScore, model, moment(indicator.valid_from));
    if (nextScoreReactionDate) {
      patch = { ...patch, decay_next_reaction_date: nextScoreReactionDate };
    }
  }
}

```

The logic follows this decision tree:
- Find the next lower score in the decay rule's points array
- If the new score is at or below `decay_revoke_score`, mark the indicator as revoked and disable detection
- Otherwise, calculate the next reaction date using `computeNextScoreReactionDate` and schedule future processing

### Step 3: Persisting Updates

The computed patch is persisted using `patchAttribute`:

```typescript
return patchAttribute(context, user, indicator.id, ENTITY_TYPE_INDICATOR, patch);

```

This updates the indicator's document in Elasticsearch with the new score, history, revocation status, and next reaction date.

## Error Handling and Resilience

The indicator decay manager implements robust error isolation. Within the processing loop, individual indicator failures are caught and logged without aborting the batch:

```typescript
try {
  await updateIndicatorDecayScore(context, DECAY_MANAGER_USER, indicator);
} catch (e) {
  logApp.error('[OPENCTI-MODULE] Error when processing decay, skipping.', { cause: e, id: indicatorsToUpdate[i].id });
  errorCount += 1;
}

```

After processing, the manager emits a summary log:

```typescript
if (errorCount > 0) {
  logApp.error('[OPENCTI-MODULE] Indicator decay manager got errors.', { errors_count: errorCount, indicators_count: indicatorsToUpdate.length });
} else {
  logApp.debug('[OPENCTI-MODULE] Indicator decay manager updated', { indicators_count: indicatorsToUpdate.length });
}

```

This design ensures that a corrupted indicator or transient database error cannot stall the decay process for thousands of other indicators.

## Code Example: Manual Trigger

For testing or administrative purposes, you can manually invoke the decay handler. This respects the same configuration and batch size limits defined in the manager:

```typescript
import { indicatorDecayHandler } from '../src/manager/indicatorDecayManager';

(async () => {
  // Directly invoke the handler – useful for unit tests or forced runs
  await indicatorDecayHandler();
  console.log('Decay run finished');
})();

```

In production, the cron scheduler automatically invokes this handler according to the `indicator_decay_manager:interval` configuration. Manual invocation is primarily used in integration tests located at [`tests/03-integration/04-manager/indicatorDecayManager-test.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/tests/03-integration/04-manager/indicatorDecayManager-test.ts).

## Summary

- The **indicator decay manager** is a scheduled background service in OpenCTI that automatically ages Indicators based on configurable Decay Rules.
- Configuration in [`indicatorDecayManager.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/indicatorDecayManager.ts) controls enablement, interval (default 60 seconds), batch size (default 10,000), and distributed locking.
- The manager queries for stale indicators using `findIndicatorsForDecay` in [`indicator-domain.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/indicator-domain.ts), selecting only non-revoked indicators whose `decay_next_reaction_date` has passed.
- Each indicator is processed via `updateIndicatorDecayScore`, which calculates the next score step, updates history, determines revocation status, and schedules the next reaction date.
- Error isolation ensures individual processing failures are logged without stopping the batch, maintaining system stability.

## Frequently Asked Questions

### How often does the indicator decay manager run?

By default, the indicator decay manager runs every **60 seconds** (60,000 milliseconds). This interval is controlled by the `indicator_decay_manager:interval` configuration key in [`indicatorDecayManager.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/indicatorDecayManager.ts). You can adjust this value based on your performance requirements and the volume of indicators in your deployment.

### What happens when an indicator's score reaches the revoke threshold?

When the computed `newStableScore` is less than or equal to the Decay Rule's `decay_revoke_score`, the manager automatically sets `revoked: true` and `x_opencti_detection: false` on the indicator. This marks the indicator as invalid and removes it from active detection rules, effectively retiring the threat intelligence once it has aged sufficiently.

### Can the indicator decay manager process indicators in parallel across multiple OpenCTI instances?

Yes, the manager supports distributed deployments through the `indicator_decay_manager:lock_key` configuration. This lock key (default: `indicator_decay_manager_lock`) prevents multiple OpenCTI instances from running the decay process simultaneously, ensuring that the same indicator is not processed twice in a clustered environment.

### Where can I find the integration tests for the indicator decay manager?

The integration tests are located at [`tests/03-integration/04-manager/indicatorDecayManager-test.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/tests/03-integration/04-manager/indicatorDecayManager-test.ts) in the OpenCTI repository. These tests demonstrate the manager's behavior by manually invoking `indicatorDecayHandler` and verifying that indicators transition through decay states correctly, including score reductions and eventual revocation.