Arcjet content moderation evaluates untrusted text for harmful content before it is stored, displayed, or forwarded to another service. Use it on user messages, tool-call results, and model outputs at the same action boundary as [Agent guards](/guards).

**What is Arcjet?** [Arcjet](https://arcjet.com) is the runtime security platform that ships with your code. Enforce budgets, stop prompt injection, detect bots, and protect personal information with Arcjet's AI security building blocks.

Content moderation is a Guard rule. There is no `protect()` / HTTP-request variant. Call it from `guard()` in JavaScript or Python, or `Guard` in Go.

When to use content moderation
------------------------------

[Section titled “When to use content moderation”](#when-to-use-content-moderation)

Use content moderation when you need a decision on text that is about to leave your control or be shown to a user, for example:

*   **User-facing chat and support assistants** – block harmful messages before they are stored or displayed.
*   **Agent tool results and model outputs** – scan generated text before it is forwarded to another tool, a customer, or a third-party API.
*   **Moderation at the action boundary** – the same `guard()` call can combine content moderation with rate limiting or prompt injection detection.

Content moderation answers a different question from [prompt injection detection](/prompt-injection) (hostile instructions aimed at the model) and [sensitive information detection](/sensitive-info) (PII and other regulated data). Combine them when a single untrusted string needs more than one check.

Guard-only rule

Content moderation inspects caller-supplied text, so it is configured in code with the Guard SDK. It is not available as a [remote rule](/remote-rules) or on `protect()`.

How Arcjet content moderation works
-----------------------------------

[Section titled “How Arcjet content moderation works”](#how-arcjet-content-moderation-works)

1.  You configure `moderateContent()` (JavaScript), `ModerateContent()` (Python), or `GuardModerateContent` (Go) once and reuse the rule.
2.  At the action boundary, bind the untrusted text and submit it with `guard()` / `Guard`.
3.  Arcjet returns `ALLOW` or `DENY`. A denial has reason `MODERATE_CONTENT`.
4.  The per-rule result is a binary `detected` / `Detected` verdict plus optional billing in `text_units`.

The result shape does not include per-category scores. For what the default policy enforces and why it focuses on sexual content for payment processor compliance, see the [content moderation policy](/content-moderation/policy).

### SDK availability

[Section titled “SDK availability”](#sdk-availability)

SDK

Public API

Notes

JavaScript / TypeScript

`moderateContent()`

`mode` is optional and defaults to `LIVE`.

Python

`ModerateContent()`

`mode` is optional and defaults to `LIVE`.

Go

`GuardModerateContent`

`Mode` is required (`ModeLive` or `ModeDryRun`). An empty `Mode` returns `ErrInvalidMode`.

### Examples

[Section titled “Examples”](#examples)

Configure the rule once, bind the untrusted text, and submit it with `guard()` / `Guard`:

(() => { class StarlightTabsRestore extends HTMLElement { connectedCallback() { const starlightTabs = this.closest('starlight-tabs'); if (!(starlightTabs instanceof HTMLElement) || typeof localStorage === 'undefined') return; const syncKey = starlightTabs.dataset.syncKey; if (!syncKey) return; const label = localStorage.getItem(\`starlight-synced-tabs\_\_${syncKey}\`); if (!label) return; const tabs = \[...starlightTabs?.querySelectorAll('\[role="tab"\]')\]; const tabIndexToRestore = tabs.findIndex( (tab) => tab instanceof HTMLAnchorElement && tab.textContent?.trim() === label ); const panels = starlightTabs?.querySelectorAll(':scope > \[role="tabpanel"\]'); const newTab = tabs\[tabIndexToRestore\]; const newPanel = panels\[tabIndexToRestore\]; if (tabIndexToRestore < 1 || !newTab || !newPanel) return; tabs\[0\]?.setAttribute('aria-selected', 'false'); tabs\[0\]?.setAttribute('tabindex', '-1'); panels?.\[0\]?.setAttribute('hidden', 'true'); newTab.removeAttribute('tabindex'); newTab.setAttribute('aria-selected', 'true'); newPanel.removeAttribute('hidden'); } } customElements.define('starlight-tabs-restore', StarlightTabsRestore); })()

*   [JavaScript / TypeScript](#tab-panel-191)
*   [Python](#tab-panel-192)
*   [Go](#tab-panel-193)

```ts
import { launchArcjet, moderateContent } from "@arcjet/guard";

const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const moderate = moderateContent();

const decision = await arcjet.guard({
  label: "tools.chat",
  rules: [moderate(userMessage)],
});

if (decision.conclusion === "DENY" && decision.reason === "MODERATE_CONTENT") {
  throw new Error("Harmful content detected – rephrase your message");
}

const result = moderate.result(decision);
// `detected` is true when harmful content was found. Billing is undefined
// when the service does not report usage. Content moderation uses text_units.
console.log(result?.detected, result?.billing?.unit, result?.billing?.count);
```

```py
import os

from arcjet.guard import ModerateContent, launch_arcjet

arcjet = launch_arcjet(key=os.environ["ARCJET_KEY"])
moderate = ModerateContent()

decision = await arcjet.guard(
    label="llm.output",
    rules=[moderate(text)],
)

if decision.conclusion == "DENY" and decision.reason == "MODERATE_CONTENT":
    raise RuntimeError("Harmful content detected – rephrase your message")

result = moderate.result(decision)
# `detected` is True when harmful content was found. Billing is None
# when the service does not report usage. Content moderation uses text_units.
print(result.detected if result else None)
if result and result.billing:
    print(result.billing.unit, result.billing.count)
```

```go
moderation, err := arcjet.GuardModerateContent(arcjet.GuardModerateContentOptions{
  Mode: arcjet.ModeLive, // required
})
if err != nil {
  return err
}

decision, err := guard.Guard(ctx, arcjet.GuardRequest{
  Label: "tools.generate",
  Rules: []arcjet.GuardRuleInput{moderation.Text(userMessage)},
})
if err != nil {
  return err
}
if decision.IsDenied() && decision.Reason == arcjet.ReasonModerateContent {
  return errors.New("content flagged by moderation")
}

// Billing is optional. Content moderation usage is measured in text_units.
if result := moderation.Result(decision); result != nil && result.Billing != nil {
  fmt.Printf("charged %d %s\n", result.Billing.Count, result.Billing.Unit)
}
```

Set `Mode` on every Guard rule. An empty `Mode` returns `ErrInvalidMode`.

For install steps and framework-specific wrappers, see the [content moderation quick start](/content-moderation/quick-start).

### Dry run mode

[Section titled “Dry run mode”](#dry-run-mode)

`mode: "DRY_RUN"` / `ModeDryRun` evaluates the rule without blocking. Use this to measure detections before switching to `"LIVE"`.

Pricing
-------

[Section titled “Pricing”](#pricing)

Content moderation is priced based on usage, measured in `text_units` when the service reports billing. A text unit is the length of the text you submit to a single moderation call, in characters, divided by 1,000 and rounded up. For a worked example, see [how text units are calculated](/content-moderation/policy#how-text-units-are-calculated). For the price per text unit, see the [pricing page](https://arcjet.com/pricing).

Discussion
----------