DNS Security
DNS Security provides DNS-level threat prevention for your managed fleet by integrating with third-party DNS filtering providers and enforcing allowlist/blocklist policies. The system ingests DNS security events from connected providers, categorizes threats, logs blocked queries, and gives you aggregated statistics to understand threat exposure across your organization. Breeze integrates with an existing DNS filtering provider you already run or subscribe to – it is not itself a DNS resolver.
You manage DNS Security from the DNS Security page in the dashboard sidebar. The page has tabs for an overview, blocked-event feed, policy management, and provider integrations. The API endpoints below back the same workflows for scripts and automations.
Integrations connect Breeze to your existing DNS filtering infrastructure. Each integration is scoped to an organization and stores encrypted API credentials for the target provider. Once connected, Breeze periodically syncs DNS security events from the provider and stores them in the dns_security_events table, where they can be queried, filtered, and analyzed. Policies define domain-level allowlists and blocklists that are pushed back to the provider through a sync pipeline.
Key Concepts
Section titled “Key Concepts”How DNS Security Works
Section titled “How DNS Security Works”-
Connect a DNS filter provider by creating an integration with the provider’s API credentials. Breeze encrypts all secrets at rest.
-
Breeze syncs events from the provider on a configurable interval. Blocked and redirected queries are stored with their action, threat category, source device, and domain. Allowed queries are stored only when the integration opts in; see Stored traffic.
-
Create policies to define allowlists and blocklists. Policies are linked to a specific integration and contain domain entries with optional reasons.
-
Policies sync to the provider through BullMQ. When you add or remove domains, a sync job is enqueued to push changes upstream to the DNS provider.
-
Analyze threat data using the stats, top-blocked, and events endpoints. Breeze automatically aggregates data for efficient querying over long time ranges.
DNS Actions
Section titled “DNS Actions”| Action | Description |
|---|---|
allowed |
DNS query was resolved normally |
blocked |
DNS query was blocked by the provider’s filtering rules |
redirected |
DNS query was redirected to a different address (e.g., a block page or sinkhole) |
Threat Categories
Section titled “Threat Categories”| Category | Description |
|---|---|
malware |
Domain associated with malware distribution or command-and-control infrastructure |
phishing |
Domain used in phishing campaigns to harvest credentials or personal data |
botnet |
Domain linked to botnet command-and-control networks |
cryptomining |
Domain associated with unauthorized cryptocurrency mining |
ransomware |
Domain used in ransomware delivery or payment infrastructure |
spam |
Domain associated with spam campaigns |
adware |
Domain serving intrusive advertising or adware payloads |
adult_content |
Domain hosting adult or explicit content |
gambling |
Domain hosting online gambling services |
social_media |
Social media platform (typically used for content filtering policies) |
streaming |
Streaming media platform (typically used for bandwidth management policies) |
unknown |
Category not determined by the provider |
Integration Providers
Section titled “Integration Providers”Breeze supports the following DNS filter providers:
| Provider | Enum Value | Required Config |
|---|---|---|
| Cisco Umbrella | umbrella |
apiKey, apiSecret |
| Cloudflare Gateway | cloudflare |
apiKey, config.accountId |
| DNSFilter | dnsfilter |
apiKey |
| Pi-hole | pihole |
apiKey, config.apiEndpoint, optional config.piholeVersion |
| AdGuard Home | adguard_home |
apiKey, apiSecret, config.apiEndpoint |
For AdGuard Home, apiKey is the admin username and apiSecret is the admin password (the provider uses HTTP Basic Auth). config.apiEndpoint is the base URL of your AdGuard Home instance (e.g., https://adguard.example.com).
For Cisco Umbrella, apiKey and apiSecret come from an Umbrella API Keys credential with the Reports scope. Breeze exchanges them for an OAuth2 bearer token and reads DNS activity from https://api.umbrella.com/reports/v2/activity. No organization ID is needed — the token’s own subject claim scopes every call to your org. config.organizationId is still accepted on existing integrations, but it is no longer required and is not sent anywhere.
For Cloudflare Gateway, apiKey is an API token for the Zero Trust account named by config.accountId. Give it Account Analytics: Read (DNS events are read from the Gateway analytics dataset through the GraphQL API) and Zero Trust: Edit (to add and remove domains on the lists named by config.blocklistId and config.allowlistId). The dataset is grouped, not per query, so events carry the Gateway location but no source IP; per-query logs with device attribution need Enterprise Logpush.
Pi-hole supports both the legacy v5 admin API and the newer v6 session-based API. Set config.piholeVersion to v5 or v6 to match your Pi-hole installation’s major version.
The meaning of apiKey also changes between versions: for Pi-hole v5, apiKey is the legacy admin API token (found under the Pi-hole web interface’s API/Web settings). For Pi-hole v6, apiKey instead holds the app password — a separate v6-specific credential generated in Pi-hole’s admin settings for API access. These two credential types are not interchangeable — pasting a v5 admin token into a v6 integration (or an app password into a v5 integration) will cause authentication to fail.
Creating an Integration
Section titled “Creating an Integration”POST /dns-security/integrationsContent-Type: application/jsonAuthorization: Bearer <token>
{ "orgId": "uuid", "provider": "umbrella", "name": "Contoso Umbrella", "description": "Cisco Umbrella for Contoso HQ", "apiKey": "your-api-key", "apiSecret": "your-api-secret", "config": { "syncInterval": 60, "retentionDays": 90, "categories": ["malware", "phishing", "botnet"] }, "isActive": true}Integration Configuration Fields
Section titled “Integration Configuration Fields”| Field | Type | Description |
|---|---|---|
orgId |
UUID | Organization ID. Auto-resolved for org-scoped tokens |
provider |
string | DNS provider: umbrella, cloudflare, dnsfilter, pihole, adguard_home |
name |
string | Human-readable name (max 200 chars) |
description |
string | Optional description (max 2,000 chars) |
apiKey |
string | Provider API key (required, max 5,000 chars). For Pi-hole, the expected value depends on config.piholeVersion — see below |
apiSecret |
string | Provider API secret (required for Cisco Umbrella) |
config.organizationId |
string | Provider’s organization ID. Optional for Cisco Umbrella — accepted for backward compatibility but not used |
config.accountId |
string | Provider’s account ID (required for Cloudflare Gateway) |
config.apiEndpoint |
string | Custom API endpoint URL (required for Pi-hole) |
config.piholeVersion |
string | Pi-hole API version: v5 or v6. Defaults to v5 if unset. Determines whether apiKey is treated as the legacy admin token (v5) or the app password (v6) |
config.syncInterval |
integer | Event sync interval in minutes (5-1440, default varies by provider) |
config.retentionDays |
integer | Days to keep this integration’s security events and daily statistics (1-365). Unset uses the deployment default, 90 days. See Data retention |
config.storeAllowedTraffic |
boolean | Also store allowed queries. Unset or false (the default) stores blocked and redirected queries only. See Stored traffic |
config.categories |
string[] | Threat categories to sync (max 100 entries) |
config.blocklistId |
string | Provider-specific blocklist identifier for policy sync |
config.allowlistId |
string | Provider-specific allowlist identifier for policy sync |
isActive |
boolean | Whether the integration is active (default true) |
Triggering a Manual Sync
Section titled “Triggering a Manual Sync”To immediately sync events from a provider instead of waiting for the next scheduled interval:
POST /dns-security/integrations/:id/syncThe response includes a jobId for tracking the sync task in BullMQ.
Sync Status and Catching Up
Section titled “Sync Status and Catching Up”Each integration records the outcome of its last sync run in lastSyncStatus: success, error (with a reason in lastSyncError), partial, or truncated. Cisco Umbrella can only return a limited number of records per request, so one run makes at most 100 requests to its activity feed and saves the events in time slices as it goes. A run that uses up those requests before reaching the present stops and records partial, and lastSyncError names the time range that has not been fetched yet, for example Reached the per-run request budget; 2026-10-22T04:25:27.166Z..2026-10-24T16:00:00.187Z not yet fetched, resumes next run. lastSync then shows how far the data reaches. The dashboard marks the integration as catching up and shows that range on hover. Nothing is skipped: the next scheduled run starts where the last one stopped and keeps the slice size that worked, so a backlog left by an outage or a paused integration catches up over the following runs. When a run reaches the present, the status returns to success. Each sync covers at most the last 30 days, because the Umbrella reporting API does not return older activity.
The Umbrella activity feed also stops paging at 10,000 records per request window, so a sync narrows the window until each one fits, down to a minimum of one second. If more than about 10,000 records fall inside a single second, the records past that limit cannot be fetched at all. The run keeps what it could fetch, moves on, and records truncated. lastSyncError lists the affected windows and how many records were fetched from each, for example 1 window held more records than the provider lets a sync page through; records past that limit were not fetched and will not be retried: 2026-10-22T04:00:00.000Z..2026-10-22T04:00:01.000Z (11000 fetched). The dashboard marks the integration as some events not fetched and shows those windows on hover. Unlike partial, a later run does not recover these records. If the same run also used up its request budget, the status is truncated and lastSyncError names the not-yet-fetched range as well. The status describes the last run only: the next run that loses no records sets it back to success, or to partial if that run runs out of requests.
Stored traffic
Section titled “Stored traffic”By default Breeze stores only blocked and redirected queries. Those are the events it acts on: threat alerts, the events list, and device context. Allowed queries are usually about 99% of a provider’s log (around a million rows a day for a busy tenant), and your provider already keeps them in its own reporting, so Breeze does not store them unless you ask it to.
To store allowed queries for an integration, open DNS Security → Integrations, click the Stored data button (the clock icon) on its row, and turn on Store allowed traffic. Or call the API:
PATCH /dns-security/integrations/:idContent-Type: application/json
{ "storeAllowedTraffic": true }The setting applies to syncs from then on. It does not fetch allowed queries for time already synced, and turning it off does not delete allowed queries that are already stored; the retention job removes them as they age out.
How each provider handles it:
| Provider | Where the action comes from | With allowed traffic off |
|---|---|---|
| Cisco Umbrella | Each activity record’s verdict (proxied is stored as redirected) |
Breeze asks Umbrella for the blocked and proxied verdicts only, so allowed records are not downloaded and do not use the per-run request budget |
| Cloudflare Gateway | Each sampled group’s resolverDecision |
Allowed groups are downloaded and discarded before they are stored |
| DNSFilter | Each record’s action / decision |
Downloaded and discarded before they are stored |
| Pi-hole (v5 and v6) | Each query’s status | Downloaded and discarded before they are stored |
| AdGuard Home | Each query’s filtering reason | Downloaded and discarded before they are stored |
When no active integration in scope stores allowed traffic, GET /dns-security/stats returns "allowedTraffic": "none" with allowedQueries and blockedRate set to null, and the Overview tab shows Not stored instead of a zero. When only some integrations store it, the response says "some" and the counts are partial. The get_dns_security AI tool reports the same field with a short note.
The coverage follows the integrations’ current setting, not the history of the rows in the window. After you turn Store allowed traffic off, allowed rows that are still stored no longer appear in the totals, which show Not stored. After you turn it on, the allowed count covers only the time since then.
Removing allowed queries stored before this change
Section titled “Removing allowed queries stored before this change”Breeze does not delete allowed queries that earlier versions stored. The nightly retention job removes them as they pass retentionDays. To free the space sooner, an operator can delete them by hand. Connect to the database as its owner (the API’s breeze_app role cannot see rows outside a tenant scope), then run this in batches until it reports DELETE 0:
DELETE FROM dns_security_eventsWHERE ctid IN ( SELECT ctid FROM dns_security_events WHERE action = 'allowed' LIMIT 5000);Add AND integration_id = '<integration id>' to the inner query to limit it to one integration. Each statement commits on its own, so it does not hold a long lock on the table the sync writes to. Run VACUUM (ANALYZE) dns_security_events; afterwards so Postgres can reuse the space. The daily statistics in dns_event_aggregations keep their historical allowed counts until retention removes them.
Data retention
Section titled “Data retention”A nightly job (22:18 UTC) deletes security events and daily statistics (aggregations) older than each integration’s config.retentionDays. When an integration has no retentionDays, the deployment default applies: 90 days, or the value of DNS_SECURITY_RETENTION_DAYS_DEFAULT. Values are clamped to 1–365.
To change the window on an existing integration, open DNS Security → Integrations and use the clock button on the integration’s row, or call the API:
PATCH /dns-security/integrations/:idContent-Type: application/json
{ "retentionDays": 30 }Send "retentionDays": null to clear the override and fall back to the default. This requires MFA and the orgs:write permission. Only retentionDays and storeAllowedTraffic can be changed after an integration is created.
The job deletes in batches, and each batch commits on its own, so it never holds a long lock on the tables the sync writes to. On a large backlog, the first runs after an upgrade may not reach the window in one night: the job logs a warning, reports the backlog to Sentry, and continues the next night. These environment variables control it:
| Variable | Default | Description |
|---|---|---|
DNS_SECURITY_RETENTION_DAYS_DEFAULT |
90 |
Retention for integrations without retentionDays, in days (1–365) |
DNS_SECURITY_RETENTION_BATCH_SIZE |
5000 |
Rows deleted per batch |
DNS_SECURITY_RETENTION_MAX_BATCHES |
400 |
Maximum batches per table, per integration, per nightly run |
Deleting an Integration
Section titled “Deleting an Integration”DELETE /dns-security/integrations/:idThe request answers 202 Accepted with { "success": true, "id": "<id>", "status": "deleting" }. The integration stops syncing right away and is shown as Deleting…. A background job then removes its daily statistics and security events in batches, then its policies and the integration itself. On a large history this can take minutes to hours; the integration disappears from the list when it is done. Sending the same DELETE again while it is deleting re-queues the job without other effect, and the nightly retention job re-queues any delete whose job was lost.
DNS Policies
Section titled “DNS Policies”Policies define domain-level allowlists and blocklists that are enforced through the connected DNS provider. Each policy is tied to a specific integration and organization.
Creating a Policy
Section titled “Creating a Policy”POST /dns-security/policiesContent-Type: application/jsonAuthorization: Bearer <token>
{ "orgId": "uuid", "integrationId": "uuid", "name": "Malware Blocklist", "description": "Block known malicious domains", "type": "blocklist", "domains": [ { "domain": "malware-example.com", "reason": "Known C2 domain" }, { "domain": "phishing-site.net", "reason": "Reported phishing" } ], "categories": ["malware", "phishing", "botnet"], "isActive": true}Policy Fields
Section titled “Policy Fields”| Field | Type | Required | Description |
|---|---|---|---|
orgId |
UUID | No | Organization ID. Auto-resolved for org-scoped tokens |
integrationId |
UUID | Yes | The DNS filter integration this policy applies to |
name |
string | Yes | Human-readable name (max 200 chars) |
description |
string | No | Optional description (max 2,000 chars) |
type |
string | Yes | blocklist or allowlist |
domains |
array | No | Domain entries (max 500). Each with domain (required) and reason (optional) |
categories |
string[] | No | Threat categories to include in this policy (max 50) |
isActive |
boolean | No | Whether the policy is active (default true) |
Policy Sync Status
Section titled “Policy Sync Status”When a policy is created or updated, its syncStatus is set to pending. A BullMQ job is enqueued to push the changes to the DNS provider. The status progresses as follows:
| Status | Meaning |
|---|---|
pending |
Changes queued, waiting for sync to the provider |
synced |
Policy successfully pushed to the provider |
error |
Sync failed. Check syncError for details |
Updating Policy Domains
Section titled “Updating Policy Domains”Add or remove domains from an existing policy without replacing the entire domain list:
PATCH /dns-security/policies/:id/domainsContent-Type: application/json
{ "add": [ { "domain": "new-bad-domain.com", "reason": "Reported by threat intel feed" } ], "remove": ["old-domain.net"]}At least one domain must be provided in add or remove. Domains are normalized to lowercase with trailing dots stripped. Duplicate domains are silently ignored. The response includes the updated domain count and lists of successfully added and removed domains.
Security Events
Section titled “Security Events”DNS security events are the individual records of DNS queries processed by your DNS filter provider. Each event captures the queried domain, the action taken, the threat category (if applicable), and the source device.
Querying Events
Section titled “Querying Events”GET /dns-security/events?start=2026-02-01T00:00:00Z&end=2026-02-15T00:00:00Z&action=blocked&category=malware&limit=50Event Query Parameters
Section titled “Event Query Parameters”| Parameter | Type | Description |
|---|---|---|
start |
ISO 8601 | Start of time window (inclusive) |
end |
ISO 8601 | End of time window (inclusive) |
action |
string | Filter by action: allowed, blocked, redirected |
category |
string | Filter by threat category |
domain |
string | Partial domain match (case-insensitive) |
deviceId |
UUID | Filter events for a specific device |
integrationId |
UUID | Filter events from a specific integration |
limit |
integer | Results per page (1-500, default 100) |
offset |
integer | Pagination offset (default 0) |
Event Response Fields
Section titled “Event Response Fields”Each event in the response includes:
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique event identifier |
orgId |
UUID | Organization ID |
integrationId |
UUID | Source integration |
deviceId |
UUID | Linked device (nullable if device not matched) |
deviceHostname |
string | Hostname of the linked device (from join) |
timestamp |
ISO 8601 | When the DNS query occurred |
domain |
string | Queried domain name |
queryType |
string | DNS query type (e.g., A, AAAA, CNAME) |
action |
string | allowed, blocked, or redirected |
category |
string | Threat category (nullable) |
threatType |
string | Provider-specific threat classification |
sourceIp |
string | IP address of the querying device |
sourceHostname |
string | Hostname of the querying device (from provider) |
providerEventId |
string | Unique event ID from the DNS provider |
metadata |
object | Additional provider-specific data |
Automatic Threat Alerts
Section titled “Automatic Threat Alerts”When a blocked event arrives with a known threat category (malware, phishing, botnet, ransomware, cryptomining), Breeze automatically raises a high-severity alert for the affected device. You don’t need a rule for this — the alert fires from the DNS Security pipeline itself.
To avoid alert storms when the same device hits the same threat category repeatedly, alerts are deduplicated per (device, category) for a 60-minute cooldown window. A second malware block on the same device within the window updates the existing alert’s “last seen” timestamp rather than raising a new one.
Threat events are also published on the internal event bus so other consumers (custom webhooks, future integrations) can subscribe to dns.threat.blocked without polling the events endpoint.
Statistics and Analytics
Section titled “Statistics and Analytics”Summary Statistics
Section titled “Summary Statistics”Get aggregated DNS security statistics including total queries, blocked rates, top blocked domains, top categories, and top offending devices:
GET /dns-security/stats?start=2026-02-01T00:00:00Z&end=2026-02-15T00:00:00Z&topN=20| Parameter | Type | Description |
|---|---|---|
start |
ISO 8601 | Start of time window |
end |
ISO 8601 | End of time window |
integrationId |
UUID | Filter by integration |
deviceId |
UUID | Filter by device |
action |
string | Filter by action type |
category |
string | Filter by threat category |
topN |
integer | Number of top entries to return (1-100, default 10) |
The response includes:
{ "summary": { "totalQueries": 125000, "blockedQueries": 3200, "allowedQueries": 121500, "redirectedQueries": 300, "blockedRate": 2.56 }, "allowedTraffic": "all", "topBlockedDomains": [...], "topCategories": [...], "topDevices": [...], "source": "aggregated"}allowedTraffic says whether the active integrations in scope store allowed queries: all, some (the allowed count and blocked rate are partial), or none. With none, allowedQueries and blockedRate are null rather than a misleading zero and 100%. See Stored traffic.
Top Blocked Domains
Section titled “Top Blocked Domains”Get a ranked list of the most frequently blocked domains:
GET /dns-security/top-blocked?start=2026-02-01T00:00:00Z&end=2026-02-15T00:00:00Z&limit=20Each entry includes the domain, its threat category, the block count, and the number of distinct devices that attempted to reach it.
Deploying DNS filtering clients (Control D, NextDNS)
Section titled “Deploying DNS filtering clients (Control D, NextDNS)”Besides integrating with a provider’s API, Breeze can install a vendor’s DNS filtering client on Windows devices. Two Breeze-pinned built-in packages are offered: Control D (Windows) and NextDNS CLI (Windows). Bring your own vendor account; Breeze does not resell or provision either service. Control D for macOS is not yet offered.
| Package | Installs to | Enrolls with |
|---|---|---|
| Control D (Windows) | C:\ControlD\ctrld.exe |
A provisioning code |
| NextDNS CLI (Windows) | C:\Program Files\nextdns\nextdns.exe |
A profile ID |
The client binaries are vendor GitHub release assets, pinned in Breeze by version and sha256. The agent verifies the sha256 when it downloads the file. Both packages need agent 0.125.0 or newer.
- Add the package to your catalog. Go to Software → Catalog → Built-in packages and choose Add to catalog on the card. This needs full partner access (organization access “all”), the
devices.executepermission and MFA. Organization-scoped users do not see the built-in cards; the partner deploys them. - Set the tenant variable per organization. Under Settings → Variables, set
controld_provisioning_code(Control D) ornextdns_profile_id(NextDNS) to your<provisioning-code>or<profile-id>. These variables are non-secret by design, and secret variables are rejected. - Deploy. Use a software deployment, or a partner-wide software policy with an allowlist rule so the client is installed and kept in place across organizations.
Compliance uses the service_running signal (the ctrld or nextdns service), so a stopped client is reported as not_running. See Software Policies.
Built-in rows are managed by Breeze: you cannot edit them, delete them or add versions. When a Breeze update pins a newer client version, provisioned packages move to it automatically on the next API start. This affects new installs only; devices that already have the client keep their version, and the vendor clients update themselves.
Uninstalling
Section titled “Uninstalling”Breeze does not uninstall zip-installed clients. Run the vendor’s uninstall command on the device:
C:\ControlD\ctrld.exe uninstall& "C:\Program Files\nextdns\nextdns.exe" uninstallControl D endpoints that you delete in the Control D dashboard also uninstall themselves.
API Reference
Section titled “API Reference”Integrations
Section titled “Integrations”| Method | Path | Description |
|---|---|---|
| GET | /dns-security/integrations |
List all integrations for the organization |
| POST | /dns-security/integrations |
Create a new DNS filter integration (requires MFA, orgs:write) |
| PATCH | /dns-security/integrations/:id |
Change retentionDays and/or storeAllowedTraffic (requires MFA, orgs:write) |
| DELETE | /dns-security/integrations/:id |
Delete the integration and all associated data in the background; answers 202 (requires MFA, orgs:write) |
| POST | /dns-security/integrations/:id/sync |
Trigger immediate event sync from the provider |
Events
Section titled “Events”| Method | Path | Description |
|---|---|---|
| GET | /dns-security/events |
List DNS security events with filtering and pagination |
Statistics
Section titled “Statistics”| Method | Path | Description |
|---|---|---|
| GET | /dns-security/stats |
Aggregated statistics: totals, block rates, top domains/categories/devices |
| GET | /dns-security/top-blocked |
Ranked list of most frequently blocked domains |
Policies
Section titled “Policies”| Method | Path | Description |
|---|---|---|
| GET | /dns-security/policies |
List all DNS policies for the organization |
| POST | /dns-security/policies |
Create a new allowlist or blocklist policy (requires orgs:write) |
| PATCH | /dns-security/policies/:id/domains |
Add or remove domains from a policy (requires orgs:write) |
Troubleshooting
Section titled “Troubleshooting”Integration created but events are not appearing.
After creating an integration, an initial sync job is enqueued via BullMQ. If the sync scheduling fails, the creation response includes a warning field explaining the issue. Verify that Redis and BullMQ workers are running. Check the integration’s lastSyncStatus and lastSyncError fields via GET /dns-security/integrations to see if the sync completed or encountered an error. Also confirm that the API key and provider configuration are correct.
Policy sync stuck in pending status.
Policy syncs are dispatched through BullMQ. If BullMQ is down or backlogged, the sync will remain in pending state. Restart the DNS sync worker and check the syncError field on the policy for specific error details. If the provider API returns an authentication error, verify the integration’s API credentials.
Policy changes left pending by an earlier release are not pushed automatically after upgrading: only new edits queue a sync, and an edit pushes just its own additions and removals. To push domains that were pending before the upgrade, remove them from the policy and add them again (or re-create the policy).
Sync error: “Host resolves to a private/blocked address”.
The integration’s host (for example a Pi-hole address) resolves to a private or blocked network address that the Breeze server will not connect to. The integration’s lastSyncError (or the policy’s syncError) shows this message and the sync stops there. Use a host the Breeze server can reach on a public address.
Stats endpoint returning unexpected results.
For time windows longer than 7 days, statistics are sourced from the dns_event_aggregations table rather than raw events. Aggregations are computed periodically and may lag behind real-time data. The source field in the response (raw or aggregated) indicates which data source was used. For the most current data over short periods, use a time window of 7 days or less.
Time range query rejected with 400 error.
The maximum query window for events and statistics is 90 days. If your start-to-end range exceeds this limit, narrow the time range. Also verify that both start and end are valid ISO 8601 timestamps and that start is before end.
Domains not blocking after adding to a blocklist policy.
After adding domains to a policy, verify the syncStatus is synced (not pending or error). If the sync failed, check the syncError field. Domains are normalized to lowercase with trailing dots removed before storage and sync. If the provider requires a specific domain format, verify it matches the normalized form. Note that DNS caching at the client or resolver level may delay enforcement of new rules.
MFA required error when creating or deleting integrations.
Creating, editing and deleting DNS filter integrations require MFA verification. Ensure the authenticated user has completed MFA for the current session. The requireMfa() middleware returns a 403 if MFA has not been verified.
Pi-hole v6 sync fails with HTTP 429 and no_seats.
Pi-hole v6’s admin API is session-based and only allows a limited number of concurrent API sessions (“seats”). If a sync attempt fails with HTTP 429 and an error body containing no_seats, all available session seats are currently in use — this is not a generic rate limit, so retrying immediately usually won’t help. This typically happens when other clients (Pi-hole’s own admin web UI, another integration, or a stale session that hasn’t expired yet) are holding the remaining seats. Wait for existing sessions to expire, close other concurrent connections to the Pi-hole API, and then retry the sync. If this happens repeatedly, consider reducing how many other tools or browser tabs stay logged into the Pi-hole admin interface at the same time.