Arcjet best practices
Here are some recommended best practices for using Arcjet effectively in your applications.
Single client instance
Section titled “Single client instance”We recommend creating a single instance of the Arcjet client object and reusing it throughout your application. This is because the SDK caches decisions and configuration to improve performance. It also means the object is created outside of the route handler to avoid creating a new instance on every request.
The pattern we recommend is to create a utility file that exports the Arcjet object and then import it where you need it.
In the JS SDKs, use withRule to attach rules specific to each route or
location in your app.
For example, in a Next.js app you might create a file /lib/arcjet.ts that
creates and exports the Arcjet instance:
import arcjet, { detectBot, fixedWindow, protectSignup, sensitiveInfo, shield, slidingWindow,} from "@arcjet/next";
// Re-export the rules to simplify imports inside handlersexport { detectBot, fixedWindow, protectSignup, sensitiveInfo, shield, slidingWindow,};
// Create a base Arcjet instance for use by each handlerexport default arcjet({ // Get your site key from https://app.arcjet.com // and set it as an environment variable rather than hard coding. // See: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables key: process.env.ARCJET_KEY!, rules: [ // You could include one or more base rules to run on every request ],});Then, in your request handler you can import the Arcjet instance and use
withRule to add any route-specific rules. protect() can then be called
inside the route handler.
import arcjet, { detectBot, fixedWindow } from "@/lib/arcjet";
// Add rules to the base Arcjet instance outside of the handler functionconst aj = arcjet .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // configured with a list of bots to allow from // https://arcjet.com/bot-list allow: [], // blocks all automated clients }), ) // You can chain multiple rules, so we'll include a rate limit .withRule( fixedWindow({ mode: "LIVE", max: 100, window: "60s", }), );
export async function GET(req: Request) { // The protect method returns a decision object that contains information // about the request. const decision = await aj.protect(req);
// ... handle the request}The Python SDK’s arcjet() / arcjet_sync() constructor takes the full rule
set at creation time — there is no with_rule() chain method on the
resulting client (that pattern only exists in the JS SDKs). To apply different
rules to different routes, create one client per rule set and import them
where you need them.
Use arcjet() for async frameworks (FastAPI, Starlette) and arcjet_sync()
for sync frameworks (Flask, Django).
import os
from arcjet import Mode, arcjet, detect_bot, shield, sliding_window
# The Python SDK has no `with_rule()` chain — every client takes its full# rule set at creation time. To apply different rules to different routes,# create one client per rule set and import them where you need them.
# Read endpoints: shield + bot detection + lenient rate limitaj_read = arcjet( # Get your site key from https://app.arcjet.com # and set it as an environment variable rather than hard coding. key=os.environ["ARCJET_KEY"], rules=[ shield(mode=Mode.LIVE), detect_bot(mode=Mode.LIVE, allow=[]), sliding_window(mode=Mode.LIVE, interval=60, max=100), ],)
# Write endpoints: same protections plus a stricter rate limitaj_write = arcjet( key=os.environ["ARCJET_KEY"], rules=[ shield(mode=Mode.LIVE), detect_bot(mode=Mode.LIVE, allow=[]), sliding_window(mode=Mode.LIVE, interval=60, max=15), ],)Then import the client you need inside each route handler and call protect()
once per request:
from fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
from lib.arcjet import aj_read
app = FastAPI()
@app.get("/items")async def list_items(request: Request): decision = await aj_read.protect(request)
if decision.is_denied(): return JSONResponse({"error": "Forbidden"}, status_code=403)
# ... handle the requestIf you only need one rule set across the whole app, a single client is fine.
Avoid Arcjet in middleware
Section titled “Avoid Arcjet in middleware”We do not recommend using Arcjet in middleware. This is because middleware lacks the context of the route handler, making it difficult to apply route-specific rules or customize responses based on the request.
Our recommendation is to call protect() in each route handler where you need
it. This offers most flexibility and allows you to customize the behavior for
each route.
Call protect() once per request
Section titled “Call protect() once per request”The Arcjet protect() function should be called only once per request.
This is only usually a problem if you are using Arcjet both in middleware and in route handlers. We do not recommend using middleware, but if you are using it then use a middleware helper to scope the call to Arcjet only to routes that need it.
Start in dry run mode
Section titled “Start in dry run mode”We recommend starting each new rule in DRY_RUN mode. When configured this way,
each rule will return its decision, but the end conclusion will always be
ALLOW. This is useful for testing and tuning your rules before switching to
LIVE.
As these are configured in code, you can use other mechanisms to set the mode.
For example, you could use an existing feature flag system to dynamically
change the rule mode. We have an example showing how to sample traffic between
DRY_RUN and LIVE modes in the
Sampling traffic blueprint.
For teams with separate security and engineering owners, keep feature-specific rules in code so developers can review them with the route or workflow they protect. Use remote rules for operational changes that SecOps needs to test, promote, or roll back against live traffic without waiting for an application deploy.
Configure proxies and load balancers
Section titled “Configure proxies and load balancers”Arcjet needs to see the original client IP address to make accurate decisions. If your application is behind a proxy or load balancer, you need to ensure that Arcjet can access the original IP address.
Most proxies and load balancers add an X-Forwarded-For header to requests that
contains the original client IP address. You need to ensure that this header is
passed to your application and that Arcjet is configured to ignore the proxy IP
addresses.
This is not necessary on platforms like Firebase, Netlify, Fly.io, or Vercel because Arcjet can auto-detect the proxy IPs for these platforms.
For other platforms, configure the proxies option when creating the Arcjet
instance. For example, if the load balancer is at 100.100.100.100 and the client IP
address is 192.168.1.1, the X-Forwarded-For header will be:
X-Forwarded-For: 192.168.1.1, 100.100.100.100You should set the proxies field to ["100.100.100.100"] so Arcjet will use
192.168.1.1 as the client IP address.
You can also specify CIDR ranges to match multiple IP addresses.
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [], proxies: [ "100.100.100.100", // A single IP "100.100.100.0/24", // A CIDR for the range ],});import os
from arcjet import arcjet
aj = arcjet( key=os.environ["ARCJET_KEY"], rules=[], proxies=[ "100.100.100.100", # A single IP "100.100.100.0/24", # A CIDR for the range ],)Proxy services like Cloudflare
Section titled “Proxy services like Cloudflare”Some providers don’t add themselves to X-Forwarded-For. Instead they pass the
real client IP in their own header — for example, Cloudflare uses
CF-Connecting-IP. For these you can pass a proxy service in the proxies
option instead of a plain IP address.
The JavaScript SDKs export a cloudflare() helper for this. When a request
arrives from a known Cloudflare IP range, Arcjet reads the real client IP from
Cloudflare’s header. This is only trusted when the connecting address is within
Cloudflare’s verified IP ranges, so the header can’t be spoofed by a direct
client.
import arcjet, { cloudflare } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [], // Read the real client IP from Cloudflare's `CF-Connecting-IP` header when // the request arrives from a Cloudflare IP range proxies: [cloudflare()],});You can combine proxy services with plain IP addresses in the same list, e.g.
proxies: ["100.100.100.0/24", cloudflare()].
Adding a Cloudflare range the SDK doesn’t know about yet
Section titled “Adding a Cloudflare range the SDK doesn’t know about yet”The Cloudflare IP ranges are bundled with the SDK and updated as new versions are
released. If Cloudflare publishes a
new range before the SDK has been updated, pass
it via the ranges option. This replaces the bundled ranges rather than
adding to them, so include the full current list plus the new range:
import arcjet, { cloudflare } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [], proxies: [ cloudflare({ // This *replaces* the ranges bundled with the SDK, so include the full // list from https://www.cloudflare.com/ips/ plus any new range ranges: [ // The current Cloudflare ranges... "173.245.48.0/20", "103.21.244.0/22", // ...plus a new range not yet bundled in the SDK "203.0.113.0/24", ], }), ],});Implement your own error handling
Section titled “Implement your own error handling”Calls to protect() will not throw an error. Arcjet is designed to fail open
so that a service issue or misconfiguration does not block all requests.
If there is an error condition when processing the rule, Arcjet will return an
ERROR result for that rule and you can check the error message on the rule’s
error result for more information. For example:
const decision = await aj.protect(req);
if (decision.isErrored()) { // Fail open: log the error and allow the request console.error("Arcjet error", decision.reason.message);}decision = await aj.protect(request)
if decision.is_error(): # Fail open: log the error and allow the request logger.error("Arcjet error: %s", decision.reason_v2.message)