Learning objectives
After reading this article you will be able to:
- Understand what a provider filters and what remains your application's problem
- Configure rate limiting, timeouts and a body size limit in a Node.js API
- Test with curl that the protection is working
On this page
A DDoS (distributed denial of service) attack uses many machines at once to leave a target unable to respond. According to CISA, attackers often use botnets, networks of hijacked devices. The effect on you is simple: legitimate users cannot reach your site, API or bot.
Code alone cannot give you full protection. Part of the attack has to be filtered before it reaches your server, and the rest you handle inside the application. The examples below show what each layer does and how to configure yours, using a Node.js API.
What kinds of DDoS attacks exist?#
Three families cover almost everything:
- Volumetric: floods the network link with traffic (layers 3 and 4 of the OSI model). Your code never even runs, because bandwidth runs out first.
- Protocol: abuses how TCP works, for example thousands of half-open connections, to exhaust state tables on the server or firewall.
- Application (layer 7): sends HTTP requests that look legitimate, like thousands of searches per second on a heavy route. It is the cheapest to launch and the one that depends most on you.
This split decides the defense. Only providers with large network capacity can absorb the first two. You reduce the third with caching, limits and efficient code.
What does your provider do, and what is left for you?#
Cloudflare, for example, describes automatic mitigation at layers 3, 4 and 7 applied at the network edge. That is the common model: a CDN or reverse proxy receives all traffic, drops what it recognizes as an attack and forwards the rest.
Even so, whatever passes the filter still reaches your code. That leaves you with:
- limiting how many requests each client can make;
- closing slow or stalled connections;
- rejecting huge request bodies;
- caching expensive responses;
- not leaving the app reachable through a path that bypasses the edge.
How do you limit requests per client?#
Rate limiting returns 429 (Too Many Requests) when a client exceeds the limit in a time window, as MDN describes. In Express, the express-rate-limit package does this. The examples use a recent Node.js version and Express 5.
npm install express express-rate-limit// server.js
import express from "express";
import { rateLimit } from "express-rate-limit";
const app = express();
// Behind 1 proxy (CDN or load balancer), use the real client IP.
// Adjust the number to your setup: see the trust proxy section.
app.set("trust proxy", 1);
// Global limit: 100 requests per minute per IP.
app.use(
rateLimit({
windowMs: 60 * 1000,
limit: 100,
standardHeaders: "draft-7",
legacyHeaders: false,
}),
);
// Sensitive routes get a lower limit.
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
standardHeaders: "draft-7",
legacyHeaders: false,
});
// JSON body capped at 100 KB.
app.use(express.json({ limit: "100kb" }));
app.get("/", (req, res) => res.json({ ok: true }));
app.post("/login", loginLimiter, (req, res) => res.json({ ok: true }));
const server = app.listen(process.env.PORT ?? 3000);
// Cut slow connections (slowloris) and requests that never finish.
server.headersTimeout = 15_000;
server.requestTimeout = 30_000;Points worth noting:
- In the package documentation,
limitis the maximum per window andwindowMsis the window length in milliseconds. The library default islimit: 5per minute, so always set your own. legacyHeaders: falseturns off the oldX-RateLimit-*headers, andstandardHeadersturns on the IETF standardized headers.- The counter lives in process memory by default. With several instances, each counts separately. For a shared limit, use an external store such as Redis.
Why does trust proxy matter so much?#
Behind a proxy, req.ip would be the proxy's address, and the rate limiter would treat all users as one: a single abusive client would block everyone. trust proxy tells Express which values in the X-Forwarded-For header to believe, according to the Express guide.
The opposite mistake is dangerous too. If you trust more hops than really exist, the client forges the header and picks any IP it wants, which defeats the rate limit. So:
- Find out how many proxies sit between the user and your app.
- Use that number for
trust proxy, nottrue, unless the last proxy overwrites the header. - Confirm by logging
req.ipon a test route from two different IPs.
Which timeouts should you configure?#
A slow-connection attack keeps sockets open by sending a few bytes at a time. The Node.js documentation says server.headersTimeout and server.requestTimeout must be non-zero to protect against denial of service when the server is not behind a reverse proxy. The default requestTimeout is 300 seconds, and the default headersTimeout is the lower of requestTimeout and 60 seconds. The code above lowers them to 15 and 30 seconds. Node checks these deadlines every 30 seconds by default (connectionsCheckingInterval), so the cut can come a little after the configured value.
If your API receives large uploads, 30 seconds for the whole body may be too short. In that case, keep a higher requestTimeout and accept uploads through a separate service.
How do you reduce the cost of each request?#
In a layer 7 attack, the damage is proportional to what each request costs. Three cheap adjustments:
- Cache: serve public pages and responses with
Cache-Controlso the edge answers without calling your code. See what Cache-Control is. - Heavy routes behind authentication: searches, exports and report generation do not need to be open to the world.
- Pagination and caps: never return a whole table or accept
limit=1000000.
The rest of API hardening, such as input validation and logs, is in the API production checklist.
How do you test that it works?#
With the server running, fire 130 requests in a row and count the response codes:
for i in $(seq 1 130); do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/
done | sort | uniq -cThe expected output with limit: 100, if you run it right after starting the server, is:
100 200
30 429For the headers timeout, send an incomplete request (without the final blank line) and wait:
(printf 'GET / HTTP/1.1\r\nHost: localhost\r\n'; sleep 60) | nc localhost 3000
# the server answers 408 Request Timeout and closes the connectionIn production, the real test is watching the logs: many 429s from a few IPs show the limit working, and traffic spikes on unexpected routes point to the attack's target.
What are the common mistakes?#
- Rate limit without
trust proxy: all users share the proxy's IP and get blocked together. trust proxy: truewithout need: the client forges its IP and escapes the limit.- App reachable by its direct IP: the attacker skips the CDN and hits the server directly. Answer only on your domain, or accept traffic only from the edge.
- One limit for everything: login and search need much lower limits than static files.
- In-memory counting with several instances: the real limit becomes N times the configured one.
How to do this on Square Cloud#
On Square Cloud, the first layer comes with every plan: DDoS mitigation at layers 3, 4 and 7 and a WAF in front of your applications, on a network with an Anycast CDN of more than 200 points of presence (based on Cloudflare) and up to 10 Gbps per server. Only port 80 of the application receives traffic, published over HTTPS on the squareweb.app subdomain or on your own domain. No mitigation makes a service immune, and the second layer stays yours, because the code is yours.
Here is where this article's examples fit. The CDN respects your application's Cache-Control, so public responses you mark as cacheable are served from the edge without calling your code, and the cache purge clears the host's cache when you ship a change. Confirm your trust proxy value with the req.ip test from two IPs, as in the section about it. If you spread one domain across several applications with load balancing (from the Standard plan), move the rate limit counter to Redis hosting, in the same datacenter, so the limit doesn't become N times what you configured.
To follow an attack, the web analytics included in every plan shows routes, countries, status codes and verified bots: a spike of 429s on a route shows the limit at work. See what the network includes on the network page and explore Redis hosting.
Infrastructure
L3/L4/L7 DDoS mitigation and a WAF on every plan
FAQs
Can I protect myself from DDoS without a provider?
Only from small attacks. A volumetric attack saturates the network link before it reaches your code, so mitigation has to happen at the edge, with someone who has the network capacity to absorb the traffic.
Does per-IP rate limiting stop a DDoS?
It stops abuse from one or a few IPs, but not a distributed attack with thousands of IPs. It is one layer of defense, alongside caching, a WAF and provider-level mitigation.
Does my Discord bot need DDoS protection?
Usually the risk is low, because the bot opens outbound connections to the Gateway and receives no public traffic. Watch any exposed port or webhook, and your machine's IP if the bot runs at home.
How do I know if I am under a DDoS attack?
Common signs are sudden slowness, request spikes from many IPs hitting the same routes, and high CPU or many open connections with no rise in real users. Confirm by reading the access logs.
More in Security
See all 10 articles- What is a brute force attack and how to defend against itA brute force attack tests passwords in bulk until one works. Learn the types (spraying, credential stuffing), how they work and the defenses, with code that runs.
- My app was hacked: what to do nowIf your app was hacked, contain access, rotate every secret and preserve the logs before cleaning up. See the right order, how to find the cause and how to recover.
- Security checklist for deploying a bot, site or APIA security checklist before you deploy a bot, site or API: secrets in environment variables, dependencies, HTTPS, headers, least privilege and logs.
- Leaked your bot token? What to do right nowIf your bot token leaked, reset it in the Discord Developer Portal (or with /token in BotFather) first. Learn the right order and how to check the damage.