IP threat intelligence
Every Arcjet decision includes analysis of the client IP. Use it to detect and deny VPNs, proxies, Tor exit nodes, hosting (data center) IPs, and other high-risk networks.
You can enforce that analysis in two ways:
- A filter rule denies matching traffic together with your other Arcjet rules.
- The
ipfield on the decision lets you customize the response in your handler, including theip.threatassessment when it is present.
IP analysis fields need data from the Arcjet Cloud API, so they are populated
when protect() calls the API. For more information about each field, see
IP analysis in the SDK reference for your framework.
IP threat signals
Section titled “IP threat signals”Arcjet labels each client IP with one or more network types and, when available, a threat intelligence assessment.
| Signal | Filter field | JavaScript | Python | Go |
|---|---|---|---|---|
| VPN | ip.src.vpn | ip.isVpn() | ip.is_vpn() | ip.IsVPN |
| Proxy | ip.src.proxy | ip.isProxy() | ip.is_proxy() | ip.IsProxy |
| Tor | ip.src.tor | ip.isTor() | ip.is_tor() | ip.IsTor |
| Hosting or data center | ip.src.hosting | ip.isHosting() | ip.is_hosting() | ip.IsHosting |
| Privacy relay | ip.src.relay | ip.isRelay() | ip_details.is_relay | ip.IsRelay |
| Known abuse | None | ip.isAbuser() | ip.is_abuser() | ip.IsAbuser |
| Service name | ip.src.service | ip.service | ip_details.service | ip.Service |
| ASN type | ip.src.asnum.type | ip.asnType | ip_details.asn_type | ip.ASNType |
asnType is one of isp, hosting, business, or education. Real users
are more often on an ISP or business network than on a hosting provider.
A hosting label means the IP belongs to a hosting provider. That raises the chance the client is automated, but an API used by other servers can see legitimate hosting IPs.
VPN, proxy, and Tor labels mean the client is hiding its origin. People use VPNs for privacy or work, so a VPN match alone is not proof of abuse. Treat it as a risk signal and decide based on the route.
Privacy relays such as Apple Private Relay are paid services tied to an
account. They are less likely to be automated than an open proxy. Don’t deny
ip.src.relay unless you intend to block those clients.
Filters do not read ip.threat. To deny on risk level, reputation, activity,
or entity, inspect the decision after protect().
When threat intelligence is available, the JavaScript SDK exposes it as
decision.ip.threat, the Python SDK as decision.ip_details.threat, and the
Go SDK as decision.IP.Threat. Responses without an assessment omit it.
| Field | JavaScript | Python | Go | Typical values |
|---|---|---|---|---|
| Risk level | riskLevel | risk_level | RiskLevel | none, low, medium, high, critical |
| Confidence | confidence | confidence | Confidence | low, medium, high |
| Reputation | reputation | reputation | Reputation | malicious, suspicious, known, safe, benign, unknown |
| Trusted infrastructure | isSafe | is_safe | IsSafe | true when the IP is trusted infrastructure |
| Network types | networkTypes | network_types | NetworkTypes | hosting, vpn, proxy, tor |
| Activities | activities | activities | Activities | brute_force, scanning, botnet |
| Entities | entities | entities | Entities | crawler, ai_crawler, scanner |
| Entity name | entityName | entity_name | EntityName | a specific entity, when identified |
| Service | service | service | Service | a known service or provider, when identified |
Python uses snake_case for the same names. Go uses PascalCase.
For more information about method signatures, availability checks, and geolocation fields, see IP analysis in the Next.js SDK reference, Node.js SDK reference, Astro SDK reference, Bun SDK reference, Nuxt SDK reference, SvelteKit SDK reference, Remix SDK reference, NestJS SDK reference, Python SDK reference, or Go SDK reference. For more information about filter expressions, see the filter field reference.
Block high-risk networks
Section titled “Block high-risk networks”The following example denies hosting IPs, VPNs, proxies, and Tor. It does not deny privacy relays.
Start the rule in DRY_RUN if you want to log matches before you enforce them.
Call protect() from a route handler, Server Component, or Server Action. This
example uses an App Router route handler.
Define the filter in astro.config.mjs:
import node from "@astrojs/node";import arcjet, { filter } from "@arcjet/astro";import { defineConfig } from "astro/config";
export default defineConfig({ adapter: node({ mode: "standalone" }), env: { validateSecrets: true }, integrations: [ arcjet({ rules: [ filter({ // Deny hosting (data center) IPs, VPNs, proxies, and Tor. // This does not deny privacy relays such as Apple Private Relay. deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"], // Block requests with `LIVE`, use `DRY_RUN` to log only. mode: "LIVE", }), ], }), ],});Arcjet protection runs on HTTP requests. That is useful for dynamic routes in Astro. It is not useful for static routes, which are pre-rendered at build time.
Astro uses middleware to intercept requests. That’s where you add Arcjet.
import arcjet, { filter } from "@arcjet/deno";
// Get your Arcjet key at <https://console.arcjet.com>.// Set it as an environment variable instead of hard coding it.const arcjetKey = Deno.env.get("ARCJET_KEY");
if (!arcjetKey) { throw new Error("Cannot find `ARCJET_KEY` environment variable");}
const aj = arcjet({ key: arcjetKey, rules: [ filter({ // Deny hosting (data center) IPs, VPNs, proxies, and Tor. // This does not deny privacy relays such as Apple Private Relay. deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"], // Block requests with `LIVE`, use `DRY_RUN` to log only. mode: "LIVE", }), ],});
Deno.serve( { port: 3000 }, aj.handler(async function (request) { const decision = await aj.protect(request);
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),);Add the Nuxt module in nuxt.config.ts:
export default defineNuxtConfig({ arcjet: { key: process.env.ARCJET_KEY, }, compatibilityDate: "2025-07-15", modules: ["@arcjet/nuxt"],});Call protect() from a server route.
import arcjet, { filter } from "@arcjet/react-router";import type { ReactNode } from "react";// @ts-expect-error: `react-router` generates such type files.import type { Route } from "../routes/+types/home";
// Get your Arcjet key at <https://console.arcjet.com>.// Set it as an environment variable instead of hard coding it.const arcjetKey = process.env.ARCJET_KEY;
if (!arcjetKey) { throw new Error("Cannot find `ARCJET_KEY` environment variable");}
const aj = arcjet({ key: arcjetKey, rules: [ filter({ // Deny hosting (data center) IPs, VPNs, proxies, and Tor. // This does not deny privacy relays such as Apple Private Relay. deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"], // Block requests with `LIVE`, use `DRY_RUN` to log only. mode: "LIVE", }), ],});
export default function Home(): ReactNode { return <>Hello world</>;}
export async function loader( loaderArguments: Route.LoaderArgs,): Promise<undefined> { const decision = await aj.protect(loaderArguments);
if (decision.isDenied()) { throw new Response("Forbidden", { status: 403 }); }}import arcjet, { filter } from "@arcjet/remix";import type { LoaderFunctionArgs } from "@remix-run/node";import type { ReactNode } from "react";
// Get your Arcjet key at <https://console.arcjet.com>.// Set it as an environment variable instead of hard coding it.const arcjetKey = process.env.ARCJET_KEY;
if (!arcjetKey) { throw new Error("Cannot find `ARCJET_KEY` environment variable");}
const aj = arcjet({ key: arcjetKey, rules: [ filter({ // Deny hosting (data center) IPs, VPNs, proxies, and Tor. // This does not deny privacy relays such as Apple Private Relay. deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"], // Block requests with `LIVE`, use `DRY_RUN` to log only. mode: "LIVE", }), ],});
export default function Home(): ReactNode { return <>Hello world</>;}
export async function loader( loaderArguments: LoaderFunctionArgs,): Promise<undefined> { const decision = await aj.protect(loaderArguments);
if (decision.isDenied()) { throw new Response("Forbidden", { status: 403 }); }}import { env } from "$env/dynamic/private";import arcjet, { filter } from "@arcjet/sveltekit";import { type RequestEvent, error } from "@sveltejs/kit";
interface HandleProperties { event: RequestEvent; resolve: Resolve;}
type Resolve = (event: RequestEvent) => Promise<Response> | Response;
// Get your Arcjet key at <https://console.arcjet.com>.// Set it as an environment variable instead of hard coding it.const arcjetKey = env.ARCJET_KEY;
if (!arcjetKey) { throw new Error("Cannot find `ARCJET_KEY` environment variable");}
const aj = arcjet({ key: arcjetKey, rules: [ filter({ // Deny hosting (data center) IPs, VPNs, proxies, and Tor. // This does not deny privacy relays such as Apple Private Relay. deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"], // Block requests with `LIVE`, use `DRY_RUN` to log only. mode: "LIVE", }), ],});
export async function handle(properties: HandleProperties): Promise<Response> { const decision = await aj.protect(properties.event);
if (decision.isDenied()) { return error(403, "Forbidden"); }
return properties.resolve(properties.event);}import os
from arcjet import Mode, arcjet, filter_requestfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
aj = arcjet( key=os.environ["ARCJET_KEY"], # Get your site key from https://console.arcjet.com rules=[ filter_request( mode=Mode.LIVE, # Blocks requests. Use Mode.DRY_RUN to log only # Deny hosting (data center) IPs, VPNs, proxies, and Tor. # This does not deny privacy relays such as Apple Private Relay. deny=[ "ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor", ], ), ],)
@app.get("/")async def index(request: Request): decision = await aj.protect(request)
if decision.is_denied(): return JSONResponse({"error": "Forbidden"}, status_code=403)
return {"message": "Hello world"}import os
from arcjet import Mode, arcjet_sync, filter_requestfrom flask import Flask, jsonify, request
app = Flask(__name__)
aj = arcjet_sync( key=os.environ["ARCJET_KEY"], # Get your site key from https://console.arcjet.com rules=[ filter_request( mode=Mode.LIVE, # Blocks requests. Use Mode.DRY_RUN to log only # Deny hosting (data center) IPs, VPNs, proxies, and Tor. # This does not deny privacy relays such as Apple Private Relay. deny=[ "ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor", ], ), ],)
@app.get("/")def index(): decision = aj.protect(request)
if decision.is_denied(): return jsonify(error="Forbidden"), 403
return jsonify(message="Hello world")The Go SDK protects net/http handlers:
package main
import ( "log" "net/http" "os"
"github.com/arcjet/arcjet-go")
var aj = must(arcjet.NewClient(arcjet.Config{ Key: os.Getenv("ARCJET_KEY"), Rules: []arcjet.Rule{ arcjet.Filter(arcjet.FilterOptions{ // Deny hosting (data center) IPs, VPNs, proxies, and Tor. // This does not deny privacy relays such as Apple Private Relay. Deny: []string{ "ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor", }, // Block requests with ModeLive, use ModeDryRun to log only. Mode: arcjet.ModeLive, }), },}))
func handler(w http.ResponseWriter, r *http.Request) { decision, err := aj.Protect(r.Context(), r) if err != nil { // Fail-open: ERROR decision plus err. Log it and continue. log.Printf("arcjet: %v", err) } else if decision.IsDenied() { http.Error(w, "Forbidden", http.StatusForbidden) return }
w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("Hello world"))}
func main() { http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":3000", nil))}
func must[T any](value T, err error) T { if err != nil { panic(err) } return value}If the decision is denied, return HTTP 403 (or your own challenge). Combine
this filter with
bot protection and
Shield when you want to deny automated
clients and common attacks on the same route.
Block VPN and proxy traffic
Section titled “Block VPN and proxy traffic”To deny only VPNs and proxies, use a narrower filter:
filter({ deny: ["ip.src.vpn or ip.src.proxy"], mode: "LIVE",})filter_request( deny=["ip.src.vpn or ip.src.proxy"], mode=Mode.LIVE,)arcjet.Filter(arcjet.FilterOptions{ Deny: []string{"ip.src.vpn or ip.src.proxy"}, Mode: arcjet.ModeLive,})To deny VPNs and leave proxies to another rule, use ip.src.vpn on its own.
Many people use VPNs for privacy or work. On a login or checkout form, that
can be a useful signal. On a public marketing page it can block real
customers. Prefer DRY_RUN first, then review matches in the
Arcjet dashboard.
Block hosting and data center IPs
Section titled “Block hosting and data center IPs”Hosting IPs are a common source of bots and scripted abuse:
filter({ deny: ["ip.src.hosting"], mode: "LIVE",})filter_request( deny=["ip.src.hosting"], mode=Mode.LIVE,)arcjet.Filter(arcjet.FilterOptions{ Deny: []string{"ip.src.hosting"}, Mode: arcjet.ModeLive,})You can also match the autonomous system type:
filter({ deny: ['ip.src.asnum.type eq "hosting"'], mode: "LIVE",})filter_request( deny=['ip.src.asnum.type eq "hosting"'], mode=Mode.LIVE,)arcjet.Filter(arcjet.FilterOptions{ Deny: []string{`ip.src.asnum.type eq "hosting"`}, Mode: arcjet.ModeLive,})Don’t use this on an API that other servers call. Those clients often originate from hosting providers.
Block Tor traffic
Section titled “Block Tor traffic”Tor exit nodes hide the client origin and are a frequent source of abuse:
filter({ deny: ["ip.src.tor"], mode: "LIVE",})filter_request( deny=["ip.src.tor"], mode=Mode.LIVE,)arcjet.Filter(arcjet.FilterOptions{ Deny: []string{"ip.src.tor"}, Mode: arcjet.ModeLive,})Tor also has legitimate privacy uses. Deny it on sensitive actions such as account creation or payments if that matches your risk tolerance.
Deny with threat intelligence
Section titled “Deny with threat intelligence”ip.threat is the assessment for that IP when Arcjet has one. Check that it
exists before you read it. Then deny on the fields that match your risk
tolerance.
These patterns run in your handler after protect(). They are not filter
expressions.
Deny high or critical risk
Section titled “Deny high or critical risk”const decision = await aj.protect(req);const threat = decision.ip.threat;
if ( threat && !threat.isSafe && (threat.riskLevel === "high" || threat.riskLevel === "critical")) { return new Response("Forbidden", { status: 403 });}decision = await aj.protect(request)details = decision.ip_detailsthreat = details.threat if details else None
if ( threat and not threat.is_safe and threat.risk_level in ("high", "critical")): return JSONResponse({"error": "Forbidden"}, status_code=403)decision, err := aj.Protect(r.Context(), r)if err != nil { log.Printf("arcjet: %v", err)} else if threat := decision.IP.Threat; threat != nil && !threat.IsSafe && (threat.RiskLevel == "high" || threat.RiskLevel == "critical") { http.Error(w, "Forbidden", http.StatusForbidden) return}isSafe / is_safe / IsSafe marks trusted infrastructure. Skip a denial
when it is true, even if riskLevel is elevated.
Deny known abuse activity
Section titled “Deny known abuse activity”activities lists observed behaviors such as brute_force, scanning, and
botnet:
const threat = decision.ip.threat;const abusive = ["brute_force", "scanning", "botnet"];
if ( threat && !threat.isSafe && threat.activities.some((activity) => abusive.includes(activity))) { return new Response("Forbidden", { status: 403 });}threat = decision.ip_details.threat if decision.ip_details else Noneabusive = {"brute_force", "scanning", "botnet"}
if threat and not threat.is_safe and abusive.intersection(threat.activities): return JSONResponse({"error": "Forbidden"}, status_code=403)if threat := decision.IP.Threat; threat != nil && !threat.IsSafe && (slices.Contains(threat.Activities, "brute_force") || slices.Contains(threat.Activities, "scanning") || slices.Contains(threat.Activities, "botnet")) { http.Error(w, "Forbidden", http.StatusForbidden) return}Deny malicious reputation
Section titled “Deny malicious reputation”reputation is the upstream label. malicious is the strongest deny signal.
suspicious is weaker – treat it as a signal to challenge or rate limit
rather than an automatic deny on every route.
const threat = decision.ip.threat;
if (threat && !threat.isSafe && threat.reputation === "malicious") { return new Response("Forbidden", { status: 403 });}threat = decision.ip_details.threat if decision.ip_details else None
if threat and not threat.is_safe and threat.reputation == "malicious": return JSONResponse({"error": "Forbidden"}, status_code=403)if threat := decision.IP.Threat; threat != nil && !threat.IsSafe && threat.Reputation == "malicious" { http.Error(w, "Forbidden", http.StatusForbidden) return}Deny automated entities on sensitive routes
Section titled “Deny automated entities on sensitive routes”entities names automated clients such as crawler, ai_crawler, and
scanner. entityName is set when Arcjet identifies a specific client.
const threat = decision.ip.threat;
if ( threat && !threat.isSafe && (threat.entities.includes("scanner") || threat.entities.includes("ai_crawler"))) { return new Response("Forbidden", { status: 403 });}threat = decision.ip_details.threat if decision.ip_details else None
if threat and not threat.is_safe and ( "scanner" in threat.entities or "ai_crawler" in threat.entities): return JSONResponse({"error": "Forbidden"}, status_code=403)if threat := decision.IP.Threat; threat != nil && !threat.IsSafe && (slices.Contains(threat.Entities, "scanner") || slices.Contains(threat.Entities, "ai_crawler")) { http.Error(w, "Forbidden", http.StatusForbidden) return}confidence tells you how sure the assessment is (low, medium, or
high). Require medium or high before you deny on a public page.
Inspect a decision
Section titled “Inspect a decision”Use decision.ip when you want to customize the response instead of denying
in a filter. Check that each field is present before you read it.
const decision = await aj.protect(req);
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 });}
const ip = decision.ip;
if (ip.isHosting()) { // Hosting and data center IPs are often automated clients.}
if (ip.isVpn() || ip.isProxy() || ip.isTor()) { // Apply your policy for anonymized traffic.}
if (ip.isAbuser()) { // The IP is associated with known abuse.}
if (ip.isRelay() && ip.hasService() && ip.service === "Apple Private Relay") { // Apple Private Relay requires a paid iCloud subscription.}
const threat = ip.threat;
if (threat) { // threat.riskLevel: none, low, medium, high, critical // threat.confidence: low, medium, high // threat.reputation: malicious, suspicious, known, safe, benign, unknown // threat.isSafe: trusted infrastructure // threat.networkTypes: hosting, vpn, proxy, tor // threat.activities: brute_force, scanning, botnet // threat.entities: crawler, ai_crawler, scanner // threat.entityName and threat.service when Arcjet identifies them}decision = await aj.protect(request)
if decision.is_denied(): return JSONResponse({"error": "Forbidden"}, status_code=403)
ip = decision.ip
if ip.is_hosting(): # Hosting and data center IPs are often automated clients. pass
if ip.is_vpn() or ip.is_proxy() or ip.is_tor(): # Apply your policy for anonymized traffic. pass
if ip.is_abuser(): # The IP is associated with known abuse. pass
if ( decision.ip_details and decision.ip_details.is_relay and decision.ip_details.service == "Apple Private Relay"): # Apple Private Relay requires a paid iCloud subscription. pass
details = decision.ip_detailsthreat = details.threat if details else None
if threat: # threat.risk_level, threat.confidence, threat.reputation # threat.is_safe, threat.network_types, threat.activities # threat.entities, threat.entity_name, threat.service passdecision, err := aj.Protect(r.Context(), r)if err != nil { http.Error(w, "Unavailable", http.StatusServiceUnavailable) return}
if decision.IsDenied() { http.Error(w, "Forbidden", http.StatusForbidden) return}
ip := decision.IP
if ip.IsHosting { // Hosting and data center IPs are often automated clients.}
if ip.IsVPN || ip.IsProxy || ip.IsTor { // Apply your policy for anonymized traffic.}
if ip.IsAbuser { // The IP is associated with known abuse.}
if ip.IsRelay && ip.Service == "Apple Private Relay" { // Apple Private Relay requires a paid iCloud subscription.}
if threat := ip.Threat; threat != nil { // threat.RiskLevel, threat.Confidence, threat.Reputation // threat.IsSafe, threat.NetworkTypes, threat.Activities // threat.Entities, threat.EntityName, threat.Service}You can also manage filter rules as
remote rules from the dashboard or
the Arcjet MCP server without a
redeploy. Use investigate-ip to look up geo, ASN, and threat intelligence
for a suspicious IP, then create a filter to deny it.
Allow a trusted relay
Section titled “Allow a trusted relay”Apple Private Relay is the most common privacy relay. If you deny
ip.src.relay, you also deny those clients. To allow the relay while still
denying other anonymized networks, inspect the service name after
protect():
const decision = await aj.protect(req);
if (decision.ip.hasService() && decision.ip.service === "Apple Private Relay") { // Allow this client.}
if (decision.ip.isVpn() || decision.ip.isProxy() || decision.ip.isTor()) { return new Response("Forbidden", { status: 403 });}decision = await aj.protect(request)
if ( decision.ip_details and decision.ip_details.service == "Apple Private Relay"): # Allow this client. passelif decision.ip.is_vpn() or decision.ip.is_proxy() or decision.ip.is_tor(): return JSONResponse({"error": "Forbidden"}, status_code=403)decision, err := aj.Protect(r.Context(), r)if err != nil { log.Printf("arcjet: %v", err)} else if decision.IP.Service == "Apple Private Relay" { // Allow this client.} else if decision.IP.IsVPN || decision.IP.IsProxy || decision.IP.IsTor { http.Error(w, "Forbidden", http.StatusForbidden) return}A filter can express the same exception:
filter({ deny: [ 'ip.src.vpn or ip.src.proxy or ip.src.tor or (ip.src.relay and ip.src.service ne "Apple Private Relay")', ], mode: "LIVE",})filter_request( deny=[ 'ip.src.vpn or ip.src.proxy or ip.src.tor or (ip.src.relay and ip.src.service ne "Apple Private Relay")', ], mode=Mode.LIVE,)arcjet.Filter(arcjet.FilterOptions{ Deny: []string{ `ip.src.vpn or ip.src.proxy or ip.src.tor or (ip.src.relay and ip.src.service ne "Apple Private Relay")`, }, Mode: arcjet.ModeLive,})