You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document is intended to help with fighting off DDoS/bad traffic targeting Adobe Commerce Cloud sites.
With Adobe Commerce Cloud, all server access logs are streamed from the web server(s) to New Relic. This means it's not possible to pull the log files directly from the server an analyze them like you would be able to on a traditional hosting environment.
To overcome this, you can use a combination of New Relic MCP and a New Relic User API Key to download all traffic data and then use Claude Code to analyze the traffic for problematic patterns.
This document assumes you also have Cloudflare sitting in front of your site, but if not, then replace the Cloudflare recommendations with whatever tool you're using (Fastly, manual rules, etc).
Steps
Connect your Claude Code instance with the Oauth New Relic MCP: https://docs.newrelic.com/docs/agentic-ai/mcp/setup/#claude-code (note: it may not be strictly necessary to us the New Relic MCP, but that's at least what I initially did in order to get Claude Code to build a script to download the log data)
In New Relic, go to "Administration > API Keys" and create a new User key.
Send Claude a prompt like the 2_initial_prompt.md file below
Once that is done, you can ask Claude Code to analyze the traffic using a prompt like 3_analyze_traffic.md
I need you to write a resumable Bash script that downloads raw access-log entries from
New Relic and writes them to a local file, for offline analysis.
Fill in these placeholders before running
[NEW_RELIC_ACCOUNT_ID] — the New Relic account ID to query (integer)
[PROJECT_ID] — the platform/project identifier used in the log path
[LOG_FILE_PATH] — the full filePath value, e.g. /var/log/platform/[PROJECT_ID]/access.log
[TIME_WINDOW] — how far back to pull (default: last 7 days)
[OUTPUT_FILE] — local output file, e.g. access_log.log
Source
New Relic account ID: [NEW_RELIC_ACCOUNT_ID]
The logs are New Relic Log events where filePath = '[LOG_FILE_PATH]'
Format is standard Apache combined log, stored in the message field, e.g.:
IP - - [29/Jun/2026:04:09:49 +0000] "GET /path HTTP/1.1" 200 1234 "referer" "user-agent"
Volume can be large (a busy site is millions of entries/day). First run a
SELECT count(*) FROM Log WHERE filePath = '[LOG_FILE_PATH]' SINCE [TIME_WINDOW]
to size the job before pulling.
Auth
Use a New Relic User API key (format NRAK-...) via NerdGraph (POST https://api.newrelic.com/graphql,
header API-Key: $NEW_RELIC_API_KEY).
The script must read the key from the NEW_RELIC_API_KEY env var — never hard-code it in the file.
The core constraints (these dictate the design — don't fight them)
NRQL caps results at 5,000 rows per query (LIMIT MAX = 5000). There is no offset/deep
pagination, so you must paginate with a timestamp cursor.
Do the pagination as a curl loop in Bash that appends each batch straight to a file on disk —
do NOT route batches through the model's context (that's the bottleneck that makes this infeasible
any other way). Use jq to extract fields from each JSON response.
Required script behavior
Accept a time window (default: [TIME_WINDOW]). Pin absolute epoch-ms START/END once at the start so
the window doesn't drift across a multi-hour run (persist them to a file).
Paginate newest→oldest: each query is
SELECT timestamp, messageId, message FROM Log WHERE filePath = '[LOG_FILE_PATH]' SINCE <START_ms> UNTIL <cursor_ms> ORDER BY timestamp DESC LIMIT MAX
After each batch, set the next cursor to min(timestamp in batch) + 1 ms.
Dedup across the millisecond boundary by messageId: many rows share the same timestamp ms, so
re-including that ms (cursor = min_ts+1) will re-fetch boundary rows — track the messageIds at the
boundary ms and skip them in the next batch so no line is written twice.
Write the raw message (the combined log line) one per line to [OUTPUT_FILE].
Checkpoint the cursor to disk every batch so the script is resumable — re-running continues from
where it left off rather than restarting.
Stop when a batch returns fewer than 5,000 rows (reached the bottom of the window).
Retry transient NerdGraph errors a few times with backoff; log per-batch progress (batch #, rows,
running total) to a run log. Add a small sleep between calls to respect rate limits.
For large windows, run it in the background (nohup) since multi-million-row pulls can take hours.
Notes / caveats to bake in
If the site sits behind a CDN/reverse proxy (e.g. Cloudflare), field 1 of each log line may be the
edge/proxy IP, not the true client IP. Verify with head -1 of an early batch; if true client IPs
are needed, check for an X-Forwarded-For / CF-Connecting-IP field (which may not be in this log).
Output ends up newest-first (per descending pagination); note this so downstream analysis can sort by
timestamp if chronological order is needed.
Sanity-check at the end: line count, first/last timestamps span the requested window, no empty lines.
Build the script, do a small smoke test (e.g. cap at 2 batches in an isolated dir to confirm pagination
dedup work), then launch the full run in the background and tell me how to monitor it.
You are analyzing a web server access log to find bot / scraper / DDoS traffic that is
overloading the origin (and, for Magento specifically, driving up backend/API cost), then
to produce concrete Cloudflare mitigations. Work methodically and report conclusions backed
by specific numbers — do NOT dump raw log lines into your replies.
Input
Log file: [LOG_FILE] (e.g. ./access_log.log)
It is an Apache/Nginx "combined" format log:
IP - - [10/Oct/2026:13:55:36 +0000] "GET /path?q=1 HTTP/1.1" 200 1234 "referer" "user-agent"
The file may be very large (millions of lines / multiple GB). Stream it with grep/awk;
never load the whole thing into context. For rotated .gz logs, gunzip -k first.
STEP 0 — Orient before analyzing (do this first, every time)
head -1 [LOG_FILE] and confirm the field layout, especially that field 1 is the client IP
and the timestamp timezone (usually +0000 = UTC).
CRITICAL CAVEAT — CDN edge IPs: if the site is behind a CDN/proxy (Cloudflare, Fastly, Akamai),
field 1 is the edge IP, not the true visitor. Check whether field-1 IPs fall in Cloudflare
ranges (104.16-31.x, 162.158.x, 172.64-71.x, 173.245.x, 188.114.x, 131.0.72.x) etc. If so:
Per-IP fingerprinting and ASN/geo attribution from this log are UNRELIABLE — say so explicitly.
Pivot to user-agent, path/param, and request-rate signals, which still work.
Note that true client IP / ASN / country requires edge analytics (e.g. Cloudflare GraphQL
httpRequestsAdaptiveGroups) or a logged CF-Connecting-IP / X-Forwarded-For field.
TIMEZONE: if you were given a "site was slow at TIME" window from a monitoring tool, that's
usually local time; the log is UTC. Convert before building any time regex (e.g. PDT 4:54 AM = 11:54 UTC).
A wrong conversion makes you analyze empty traffic and wrongly conclude "nothing's there."
The core insight (what you're hunting for)
Origin overload is almost always a few identifiable traffic patterns, not mysterious load. On Magento,
three recur:
Faceted-navigation crawl (the usual root cause). Clean URLs (/cat.html) are served from
full-page cache. The moment a filter query string is appended (/cat.html?manufacturer=X&caliber=Y)
the request is uncacheable and falls through to PHP+MySQL (and any search/recommendation API),
running a fresh layered-navigation query EVERY time. A crawler walking filter permutations =
tens of thousands of uncacheable DB/API hits. This produces intermittent spikes.
Admin-token brute-force — POST flood to /rest/V1/integration/admin/token (expensive bcrypt by design).
REST API scraping/abuse — high volume to /rest/.../products, /rest/.../orders, GraphQL, etc.
The driver is usually a distributed datacenter/VPS bot fleet (HostRoyale, Vultr, DigitalOcean,
Zenlayer, OVH, etc.) and/or offshore residential proxies, hiding behind rotating realistic browser
user-agents. Real shoppers on a US store skew mobile/residential; a surge of desktop UAs or
datacenter ASNs hitting filtered category pages is the tell.
STEP 1 — Whole-file profile (one efficient pass)
Run a single streaming awk pass to get: total requests; per-day volume; status-code distribution;
method distribution; cacheable (no ?) vs uncacheable (has ?) split; counts for GraphQL, /rest/,
admin-token, search, and faceted .html? requests; the ampersand distribution of .html? URLs;
the top filter parameter names; bot-classified share; and the top user-agents. (Write the aggregates
to a small file and sort down to top-N so nothing huge hits context.)
STEP 2 — Profile any suspicious window
If there's a slow window or a day that spikes, build a TIME_REGEX for it and run window_traffic.sh
(below) to see the per-minute spike shape and the top source IPs/UAs in that window. Compare a
"during" window to a "before" window and compute which user-agents / paths / params GREW.
STEP 3 — Fingerprint top suspects
For each top IP (or, if IPs are CDN-masked, each top UA), run ip_profile.sh (below). Tells:
UA rotation — one IP emitting dozens of slightly-varied browser UAs = scraper using a rotation
library. Decisive bot signal a static blocklist can't catch.
Datacenter origin — cloud/VPS ASNs for a retail site are almost never real shoppers.
Status mix — 401/429 floods = brute-force being rate-limited; 503/504 = origin already shedding load.
96%+ filtered category views, or systematic category-tree walking = crawler, not human.
STEP 4 — Hunt the Magento vectors
Run facet_scan.sh and magento_vectors.sh (below).
On the admin-token endpoint, check the status mix: ANY 200 means a credential SUCCEEDED — treat as a
breach and tell me to rotate that credential. All-401/429 = failed + rate-limited (no breach).
The ampersand distribution matters for Cloudflare rule tuning (see mitigations).
STEP 5 — Correlate
Line up suspect volume + 503/504 timeline against the slow window. A clean story: suspects jump from
~10 req/hr to thousands exactly when errors spike. Distinguish CHRONIC load (steady all day) from ACUTE
bursts (what caused this incident).
STEP 6 — Recommend Cloudflare mitigations
Prefer rate-limit or Managed Challenge over hard block for GET traffic to product/category pages
(a hard block on a faceted URL can catch a real shopper who clicked a filter). Reserve hard Block for
unambiguous abuse (admin-token flood; a pure-datacenter ASN; a non-browser automation UA).
Produce concrete, copy-paste, Expression-Builder-safe rules. The Builder rejects nested
AND (a OR b OR c) — flatten so each OR term is self-contained.
Faceted-nav rate limit (match PARAM NAMES, not ampersand count). A common rule like
http.request.uri.query wildcard r"*&*&*&*" only matches 3+ ampersands (4+ params) and MISSES
single-parameter facet URLs (?manufacturer=X, zero ampersands) — often the bulk of the attack.
Derive the real param names from facet_scan.sh output and match them directly. Exclude pagination
and ad params (p=, is_scroll, utm_, gclid, fbclid). Threshold low (~20 req/min/IP),
action Managed Challenge.
Datacenter / cloud ASN challenge or block (when traffic concentrates in hosting ASNs). Confirm
ASNs with whois. not cf.client.bot spares verified good bots.
Example: (ip.geoip.asnum in {AS1 AS2 ...} and http.request.method eq "GET" and not ip.src in $allowlist)
Admin-token lockdown:http.request.method eq "POST" and http.request.uri.path eq "/rest/V1/integration/admin/token" → Block or rate-limit ~5/min; better, IP-allowlist to known integrations.
REST/GraphQL scraping: rate-limit /rest/V1/orders, /rest/V1/products, graphql per IP.
Geo (if the store ships to one country): Managed Challenge non-domestic traffic to .html.
Also note per-IP rate limits are weak against a DISTRIBUTED fleet (each IP stays under threshold) —
ASN/UA/geo matching matters more there.
Defense-in-depth (origin side): robots.txt: Disallow: /*?; review Magento layered-nav crawl settings /
add rel="nofollow" to filter links; raise cache-hit on clean category pages; add %v (vhost) to the
Apache LogFormat if this is a multi-store server (you often CANNOT attribute a request to a domain
otherwise — be honest about that limitation).
Report structure (end with this)
Findings: <window/scope>
Verdict — one-paragraph plain answer: what the traffic is and whether it's a concern.
Attack/load vectors — each: what it is, evidence (counts, status, timeline), chronic vs acute.
Top offenders — IPs/UAs/ASNs with counts + UA-rotation evidence + what they targeted.
Timeline correlation — suspect volume + errors vs the window.