DEV Community

Cover image for What small Node teams should log in the AI-attack era
ApiTraffic
ApiTraffic

Posted on Originally published at apitraffic.io AI-assisted

What small Node teams should log in the AI-attack era

What small Node teams should log in the AI-attack era

Your API's traffic isn't mostly human anymore. Here's what to log so you can tell the difference.

Imperva's 2026 Bad Bot Report puts bots at more than 53% of all web traffic, with 40% classified as malicious. It also reports that 27% of attacks now target APIs and identity systems, and that AI-driven bot attacks grew 12.5x. AI tooling makes recon, scraping and credential stuffing cheap to write and cheap to run. Scanners don't check your headcount before they send requests. A small team just has less time to look at what arrives.

You don't need a security team to see this traffic. You need the right fields on every request, and a few patterns you know how to spot.

1. What your host logs miss

The default logs on Render, Railway, Fly or a bare VPS usually give you one line per request: timestamp, method, path, status, maybe latency. That's enough to see that something got slow. It's not enough to answer the questions an attack raises:

  • Who was calling? Not just the IP, but which user or API key, if any.
  • What did they send? The request body is where injection attempts and bad payloads live.
  • What else did they do? One request to /v1/users/1042 is normal. Four hundred requests to /v1/users/1 through /v1/users/400, from one caller, in two minutes, is not. You only see that if you can group requests by caller and look at them in order.

Host logs answer the first question partially and the other two not at all.

2. The minimum to capture for every request

If you log nothing else, log these:

Field Why it matters
method and path What was asked for. Keep the raw path and the route template (/v1/users/:id) so you can group.
status 401, 403, 404 and 429 clusters are the earliest signs of probing.
latency_ms Scrapers and expensive queries show up as load before they show up as errors.
ip The caller's address, correctly resolved behind your proxy.
user_id or API key ID Turns "an IP" into "an account", which is what you'll actually act on. Log an ID, never the token.
user_agent Easy to fake, but useful for grouping. A lot of automation doesn't bother to fake it.
request_size Oversized bodies and bulk-export requests stand out.

The ASN (the network an IP belongs to) is worth having too, if your edge (Cloudflare, a load balancer) adds it as a header. A sudden wave from a hosting provider your users never come from is a strong signal. You don't need to resolve it yourself.

3. Five patterns to watch

All of these are cheap to detect once the fields above are in one searchable place. The log lines below are illustrative, using documentation IP ranges.

Login bursts. Credential stuffing looks like a lot of POST /v1/auth/login with 401s from a small set of IPs, or a slow trickle from many.

203.0.113.24  POST /v1/auth/login  401  38ms
203.0.113.24  POST /v1/auth/login  401  41ms
203.0.113.24  POST /v1/auth/login  401  36ms   (x 412 in 10 min)
Enter fullscreen mode Exit fullscreen mode

Sequential ID walking. One caller requesting /v1/users/1, /2, /3... This is how Broken Object Level Authorization gets found and exploited, and it's #1 on the OWASP API Security Top 10 (2023). If the responses are 200, you have a data leak, not a probe.

198.51.100.7  GET /v1/users/1041  200
198.51.100.7  GET /v1/users/1042  200
198.51.100.7  GET /v1/users/1043  200
Enter fullscreen mode Exit fullscreen mode

Pagination scraping. GET /v1/products?page=1 through page=900 at a steady rate, often with a generic or missing user agent. It's not an exploit, but it costs you compute and can lift your whole catalogue. OWASP lists this class of problem as Unrestricted Resource Consumption (API4:2023).

404 sweeps. Requests for paths you never built: /.env, /.git/config, /admin, /wp-login.php. Almost always automated. Mostly harmless on a Node API, until one of them returns 200.

192.0.2.91  GET /.env            404
192.0.2.91  GET /.git/config     404
192.0.2.91  GET /admin           404
192.0.2.91  GET /api/v1/.env     404
Enter fullscreen mode Exit fullscreen mode

New networks. Traffic that suddenly arrives from an ASN or country your users have never come from, often a cloud provider. On its own it proves nothing. Combined with any pattern above, it's the caller to look at first.

4. When to block and when to watch

Not everything that looks automated should be blocked. Some of it is your own monitoring, a partner integration, or a search engine.

  • Watch first. Log and alert on the patterns above for a week before you block anything. You'll find the legitimate automation you forgot about.
  • Rate-limit sensitive routes. Login, password reset, signup and anything that sends email or SMS. Ten attempts per IP per minute on login is a reasonable start, and it's the example ApiTraffic's own docs use.
  • Block what's never legitimate. Paths like /.env on an API that has no such file. IPs you've confirmed are abusive.
  • Allow-list what must never break. Health checks, your own services, known partners.

The goal isn't zero bot traffic. It's knowing which bots you have, and making the expensive ones pay a 429.

5. Do it yourself in Express

Here's a minimal version: structured logs with the fields above, a stricter rate limit on login, and a cheap 404-sweep block. It assumes express-rate-limit and that your app runs behind one proxy.

const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();
app.set('trust proxy', 1); // trust exactly one proxy hop, so req.ip is the real client
app.use(express.json({ limit: '100kb' }));

// 1. One structured log line per request
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    console.log(JSON.stringify({
      ts: new Date().toISOString(),
      method: req.method,
      path: req.originalUrl.split('?')[0],
      route: req.route ? req.baseUrl + req.route.path : null,
      status: res.statusCode,
      latency_ms: Number(process.hrtime.bigint() - start) / 1e6,
      ip: req.ip,
      user_id: req.user?.id ?? null,     // set by your auth middleware
      ua: req.get('user-agent') ?? null,
      req_bytes: Number(req.get('content-length') ?? 0),
      asn: req.get('x-asn') ?? null,     // only if your edge sets and overwrites it
    }));
  });
  next();
});

// 2. Block paths that are never legitimate on this API
const probe = /^\/(\.env|\.git|wp-login\.php|phpmyadmin)/i;
app.use((req, res, next) => (probe.test(req.path) ? res.sendStatus(404) : next()));

// 3. Tighter limit on login
app.use('/v1/auth/login', rateLimit({ windowMs: 60_000, limit: 10 }));

app.use('/v1', apiRouter);
app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Then point your log drain somewhere searchable and write the queries: 401s on login grouped by IP, distinct IDs per caller on /v1/users/:id, 404s per IP per hour.

Two things to get right:

  • trust proxy. If you trust every hop, a caller can write any IP into X-Forwarded-For and your logs and rate limits will believe it. Trust only the proxies you actually run behind.
  • The body. This logger leaves bodies out on purpose. If you add them, redact first (see section 7).

This works. The cost is that you now maintain a logger, a log pipeline, the queries and the alerting, and the rate limit counts per process unless you add a shared store.

6. Or do it with ApiTraffic

ApiTraffic captures every request and response from one middleware, and the same SDK carries a firewall. The quickstart takes about five minutes.

npm install @apitraffic/node
Enter fullscreen mode Exit fullscreen mode
const apitraffic = require('@apitraffic/node');

app.use(express.json());
app.use(apitraffic.express()); // after body parsers, before routes
app.use('/v1', apiRouter);
Enter fullscreen mode Exit fullscreen mode

It reads API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET from the environment. To tie requests to accounts, tag them in a route or your auth middleware:

apitraffic.tag('userId', String(req.user.id));
Enter fullscreen mode Exit fullscreen mode

The patterns above become searches in the request log:

url_path == "/v1/auth/login" AND status_code == 401
status_code == 404 AND url_path like "/.env*"
tag:userId == "1042" AND status_code >= 400
Enter fullscreen mode Exit fullscreen mode

And the blocking side lives in a firewall policy, pushed to your servers without a deploy:

  • Rate limits per IP, header (an API key, say) or path. Over the limit gets a 429 with Retry-After.
  • Access lists to allow or deny by IP or CIDR, country, ASN (from a header your edge sets), user agent or path.
  • Behavioral protection that scores each caller over a window on exactly these signals: distinct object IDs per endpoint, 401/403 counts, 404 counts and distinct endpoints. Callers who cross the threshold get a temporary deny on every server.
  • Monitor mode first, so you can see what a policy would block before it blocks anything.

7. Redact before it leaves your server

Capturing more detail means capturing more you shouldn't keep: tokens, passwords, emails, card numbers.

If you roll your own logger, never log raw Authorization or Cookie headers, strip password and token fields from bodies before you write them, and keep body logging off for routes that handle credentials.

In ApiTraffic, credential headers such as Authorization are always masked. Redaction rules set in the dashboard can run at the API, inside your process, so the value never leaves your server. They're pushed to connected servers when you save, with no deploy.

The short version

Log method, path, status, latency, IP, user ID, user agent and size on every request. Watch login bursts, ID walking, pagination scraping, 404 sweeps and new networks. Watch before you block. Rate-limit what's sensitive.

What small Node teams should log in the AI-attack era

Your API's traffic isn't mostly human anymore. Here's what to log so you can tell the difference.

Imperva's 2026 Bad Bot Report puts bots at more than 53% of all web traffic, with 40% classified as malicious. It also reports that 27% of attacks now target APIs and identity systems, and that AI-driven bot attacks grew 12.5x. AI tooling makes recon, scraping and credential stuffing cheap to write and cheap to run. Scanners don't check your headcount before they send requests. A small team just has less time to look at what arrives.

You don't need a security team to see this traffic. You need the right fields on every request, and a few patterns you know how to spot.

1. What your host logs miss

The default logs on Render, Railway, Fly or a bare VPS usually give you one line per request: timestamp, method, path, status, maybe latency. That's enough to see that something got slow. It's not enough to answer the questions an attack raises:

  • Who was calling? Not just the IP, but which user or API key, if any.
  • What did they send? The request body is where injection attempts and bad payloads live.
  • What else did they do? One request to /v1/users/1042 is normal. Four hundred requests to /v1/users/1 through /v1/users/400, from one caller, in two minutes, is not. You only see that if you can group requests by caller and look at them in order.

Host logs answer the first question partially and the other two not at all.

2. The minimum to capture for every request

If you log nothing else, log these:

Field Why it matters
method and path What was asked for. Keep the raw path and the route template (/v1/users/:id) so you can group.
status 401, 403, 404 and 429 clusters are the earliest signs of probing.
latency_ms Scrapers and expensive queries show up as load before they show up as errors.
ip The caller's address, correctly resolved behind your proxy.
user_id or API key ID Turns "an IP" into "an account", which is what you'll actually act on. Log an ID, never the token.
user_agent Easy to fake, but useful for grouping. A lot of automation doesn't bother to fake it.
request_size Oversized bodies and bulk-export requests stand out.

The ASN (the network an IP belongs to) is worth having too, if your edge (Cloudflare, a load balancer) adds it as a header. A sudden wave from a hosting provider your users never come from is a strong signal. You don't need to resolve it yourself.

3. Five patterns to watch

All of these are cheap to detect once the fields above are in one searchable place. The log lines below are illustrative, using documentation IP ranges.

Login bursts. Credential stuffing looks like a lot of POST /v1/auth/login with 401s from a small set of IPs, or a slow trickle from many.

203.0.113.24  POST /v1/auth/login  401  38ms
203.0.113.24  POST /v1/auth/login  401  41ms
203.0.113.24  POST /v1/auth/login  401  36ms   (x 412 in 10 min)
Enter fullscreen mode Exit fullscreen mode

Sequential ID walking. One caller requesting /v1/users/1, /2, /3... This is how Broken Object Level Authorization gets found and exploited, and it's #1 on the OWASP API Security Top 10 (2023). If the responses are 200, you have a data leak, not a probe.

198.51.100.7  GET /v1/users/1041  200
198.51.100.7  GET /v1/users/1042  200
198.51.100.7  GET /v1/users/1043  200
Enter fullscreen mode Exit fullscreen mode

Pagination scraping. GET /v1/products?page=1 through page=900 at a steady rate, often with a generic or missing user agent. It's not an exploit, but it costs you compute and can lift your whole catalogue. OWASP lists this class of problem as Unrestricted Resource Consumption (API4:2023).

404 sweeps. Requests for paths you never built: /.env, /.git/config, /admin, /wp-login.php. Almost always automated. Mostly harmless on a Node API, until one of them returns 200.

192.0.2.91  GET /.env            404
192.0.2.91  GET /.git/config     404
192.0.2.91  GET /admin           404
192.0.2.91  GET /api/v1/.env     404
Enter fullscreen mode Exit fullscreen mode

New networks. Traffic that suddenly arrives from an ASN or country your users have never come from, often a cloud provider. On its own it proves nothing. Combined with any pattern above, it's the caller to look at first.

4. When to block and when to watch

Not everything that looks automated should be blocked. Some of it is your own monitoring, a partner integration, or a search engine.

  • Watch first. Log and alert on the patterns above for a week before you block anything. You'll find the legitimate automation you forgot about.
  • Rate-limit sensitive routes. Login, password reset, signup and anything that sends email or SMS. Ten attempts per IP per minute on login is a reasonable start, and it's the example ApiTraffic's own docs use.
  • Block what's never legitimate. Paths like /.env on an API that has no such file. IPs you've confirmed are abusive.
  • Allow-list what must never break. Health checks, your own services, known partners.

The goal isn't zero bot traffic. It's knowing which bots you have, and making the expensive ones pay a 429.

5. Do it yourself in Express

Here's a minimal version: structured logs with the fields above, a stricter rate limit on login, and a cheap 404-sweep block. It assumes express-rate-limit and that your app runs behind one proxy.

const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();
app.set('trust proxy', 1); // trust exactly one proxy hop, so req.ip is the real client
app.use(express.json({ limit: '100kb' }));

// 1. One structured log line per request
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    console.log(JSON.stringify({
      ts: new Date().toISOString(),
      method: req.method,
      path: req.originalUrl.split('?')[0],
      route: req.route ? req.baseUrl + req.route.path : null,
      status: res.statusCode,
      latency_ms: Number(process.hrtime.bigint() - start) / 1e6,
      ip: req.ip,
      user_id: req.user?.id ?? null,     // set by your auth middleware
      ua: req.get('user-agent') ?? null,
      req_bytes: Number(req.get('content-length') ?? 0),
      asn: req.get('x-asn') ?? null,     // only if your edge sets and overwrites it
    }));
  });
  next();
});

// 2. Block paths that are never legitimate on this API
const probe = /^\/(\.env|\.git|wp-login\.php|phpmyadmin)/i;
app.use((req, res, next) => (probe.test(req.path) ? res.sendStatus(404) : next()));

// 3. Tighter limit on login
app.use('/v1/auth/login', rateLimit({ windowMs: 60_000, limit: 10 }));

app.use('/v1', apiRouter);
app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Then point your log drain somewhere searchable and write the queries: 401s on login grouped by IP, distinct IDs per caller on /v1/users/:id, 404s per IP per hour.

Two things to get right:

  • trust proxy. If you trust every hop, a caller can write any IP into X-Forwarded-For and your logs and rate limits will believe it. Trust only the proxies you actually run behind.
  • The body. This logger leaves bodies out on purpose. If you add them, redact first (see section 7).

This works. The cost is that you now maintain a logger, a log pipeline, the queries and the alerting, and the rate limit counts per process unless you add a shared store.

6. Or do it with ApiTraffic

ApiTraffic captures every request and response from one middleware, and the same SDK carries a firewall. The quickstart takes about five minutes.

npm install @apitraffic/node
Enter fullscreen mode Exit fullscreen mode
const apitraffic = require('@apitraffic/node');

app.use(express.json());
app.use(apitraffic.express()); // after body parsers, before routes
app.use('/v1', apiRouter);
Enter fullscreen mode Exit fullscreen mode

It reads API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET from the environment. To tie requests to accounts, tag them in a route or your auth middleware:

apitraffic.tag('userId', String(req.user.id));
Enter fullscreen mode Exit fullscreen mode

The patterns above become searches in the request log:

url_path == "/v1/auth/login" AND status_code == 401
status_code == 404 AND url_path like "/.env*"
tag:userId == "1042" AND status_code >= 400
Enter fullscreen mode Exit fullscreen mode

And the blocking side lives in a firewall policy, pushed to your servers without a deploy:

  • Rate limits per IP, header (an API key, say) or path. Over the limit gets a 429 with Retry-After.
  • Access lists to allow or deny by IP or CIDR, country, ASN (from a header your edge sets), user agent or path.
  • Behavioral protection that scores each caller over a window on exactly these signals: distinct object IDs per endpoint, 401/403 counts, 404 counts and distinct endpoints. Callers who cross the threshold get a temporary deny on every server.
  • Monitor mode first, so you can see what a policy would block before it blocks anything.

7. Redact before it leaves your server

Capturing more detail means capturing more you shouldn't keep: tokens, passwords, emails, card numbers.

If you roll your own logger, never log raw Authorization or Cookie headers, strip password and token fields from bodies before you write them, and keep body logging off for routes that handle credentials.

In ApiTraffic, credential headers such as Authorization are always masked. Redaction rules set in the dashboard can run at the API, inside your process, so the value never leaves your server. They're pushed to connected servers when you save, with no deploy.

The short version

Log method, path, status, latency, IP, user ID, user agent and size on every request. Watch login bursts, ID walking, pagination scraping, 404 sweeps and new networks. Watch before you block. Rate-limit what's sensitive.

Start free and see the last hour of what's hitting your API: apitraffic.io

Top comments (0)