FUTIA
GÜVENLIK10 min read

Blocking Bot Attacks with Nginx 444 Status Code: Config Examples

Nginx's 444 status code instantly closes connections, reducing server load by up to 80%. I explain with real config examples and measurements.

Blocking Bot Attacks with Nginx 444 Status Code: Config Examples
Miraç Eroğlu
April 24, 2026

Last month on doktorbul.com, there was an endpoint receiving 180 requests per second for 24 hours straight. When I checked the access log, they all had the same user agent, all from the same IP block. I enabled rate limiting on Cloudflare, it slowed down a bit but 60% of the traffic was still reaching the server. Then I started blocking with Nginx's 444 status code. Server CPU usage dropped from 73% to 22%. I cut it off without even establishing a connection. Nginx's 444 code isn't standard HTTP, it's its own special behavior. No response body, no headers, just instantly closes the TCP connection. No information returns to the attacker, you don't even have to log it if you want. In this article, I'm sharing real config examples, how to use it in which scenarios, measurement methods, and my experiences from FUTIA projects.

What is Nginx 444 Status Code and How Does It Work

Nginx's 444 code isn't defined in RFC 7231. It's an Nginx-specific directive. Normal HTTP responses (200, 404, 500) return headers and body, information goes to the attacker. When you use 444, Nginx silently closes the connection, sends no data. After the TCP handshake completes, the client sends an HTTP request, Nginx reads it but closes the connection without responding. A "connection reset" or timeout error occurs on the client side.

This behavior is especially effective against brute force, scraping bots, and DDoS attacks. Because it gives no feedback to the attacker. When a normal 403 or 429 is returned, the attacker immediately reacts like "okay, I'm blocked, let me change IP." With 444, they can't understand what happened, they keep trying for a while, then give up.

There's also the log load. If you choose not to write to access log (access_log off;), disk I/O decreases. If you have 500 bot requests per second and you're logging all of them, the daily log file reaches gigabytes. If you cut with 444 and then disable logging, you gain both disk and CPU savings.

444 vs 403 vs 429: Which One When

403 Forbidden: Says "you don't have permission to access this resource," returns headers and body. The attacker gets information.

429 Too Many Requests: Says "you exceeded the rate limit, slow down," tells when to retry with the Retry-After header. The attacker uses this information.

444: Says nothing, closes the connection. The attacker is left in the dark.

Don't return 444 to legitimate users. Because they'll see an ambiguous message like "connection error" in the browser, they won't understand what happened. Apply 444 only to bots, scrapers, or clearly malicious traffic.

Basic Config: User Agent and Referrer-Based Blocks

First step is simple: block known bot user agents. Example config:

map $http_user_agent $block_ua {
 default 0;
 ~*curl 1;
 ~*wget 1;
 ~*python-requests 1;
 ~*Go-http-client 1;
 ~*scrapy 1;
 ~*bot 1;
 ~*spider 1;
 "" 1; # Empty user agent
}

server {
 listen 80;
 server_name ornek.com;

 if ($block_ua) {
 return 444;
 }

 location / {
 proxy_pass http://backend;
 }
}

This config blocks tools like curl, wget, python-requests. But be careful: you might also block legitimate bots like Google bot, Bing bot. You need to whitelist them:

map $http_user_agent $block_ua {
 default 0;
 ~*Googlebot 0;
 ~*Bingbot 0;
 ~*baiduspider 0;
 ~*YandexBot 0;
 ~*curl 1;
 ~*wget 1;
 ~*python 1;
 "" 1;
}

Referrer-based blocking also works. You can cut traffic from spam sites:

map $http_referer $block_ref {
 default 0;
 ~*spam-site\.com 1;
 ~*casino 1;
 ~*viagra 1;
}

server {
 if ($block_ref) {
 return 444;
 }
}

At FUTIA, we used a similar config for kamupersonelhaber.com. We cut traffic from spam referrers with 444, saved 120 GB of bandwidth monthly. Access log size decreased by 40%.

IP-Based Blocks and Geo-Blocking

To block specific IPs or IP blocks, you use the deny directive, but returning 444 requires a slightly different approach:

geo $block_ip {
 default 0;
 192.168.1.50 1;
 10.0.0.0/8 1;
 203.0.113.0/24 1;
}

server {
 if ($block_ip) {
 return 444;
 }
}

For geo-blocking, you use the GeoIP module. You need to load the GeoIP2 or GeoLite2 database into Nginx. Example:

geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
 $geoip2_country_code country iso_code;
}

map $geoip2_country_code $block_country {
 default 0;
 CN 1; # China
 RU 1; # Russia
 KP 1; # North Korea
}

server {
 if ($block_country) {
 return 444;
 }
}

This config cuts all traffic from China, Russia, and North Korea. But be careful: you'll also block VPN users. If you don't have a global service and only serve Turkey, it might make sense.

At FUTIA, we limited non-Turkey traffic for diolivo.com.tr. It's an e-commerce site, only sells to Turkey. Bot traffic from outside Turkey decreased by 90%, server cost dropped by 80 EUR per month.

Combination with Rate Limiting: limit_req and 444

Nginx's limit_req module determines how many requests per second or minute are accepted. Normally it returns 503 to requests exceeding the limit, but you can make it return 444:

limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_status 444;

server {
 location / {
 limit_req zone=general burst=20 nodelay;
 proxy_pass http://backend;
 }
}

Here $binary_remote_addr keeps a separate counter for each IP. rate=10r/s means 10 requests per second, burst=20 means a buffer for 20 instant requests. When a request exceeding the limit comes, it returns 444 instead of 503 thanks to limit_req_status 444.

But be careful: limit_req_status is a global directive, it applies to all locations. If you want to return 444 only to specific endpoints, you need to use different zones:

limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=static:10m rate=50r/s;

server {
 location /api/ {
 limit_req zone=api burst=10;
 limit_req_status 444;
 proxy_pass http://api_backend;
 }

 location /static/ {
 limit_req zone=static burst=100;
 # Will return 503 here
 }
}

At FUTIA, we put a 3 requests per second limit on the /api/ilanlar endpoint for memuratamalari.com. A scraper bot tried for minutes, always got 444, finally gave up. There were 40,000 lines of 444 records in the log, all from the same IP. We manually added that IP to the geo block.

Access Log Filtering: Not Logging 444s

If thousands of 444 requests are logged, the disk fills up, log analysis becomes difficult. To log only requests other than 444, you use map:

map $status $loggable {
 444 0;
 default 1;
}

server {
 access_log /var/log/nginx/access.log combined if=$loggable;

 if ($block_ua) {
 return 444;
 }
}

This config doesn't write requests returning 444 to access.log. Only normal responses like 200, 404, 500 are logged. Disk I/O decreases, log file stays small.

Alternatively, you can write 444s to a separate log file:

map $status $blocked_log {
 444 /var/log/nginx/blocked.log;
 default /dev/null;
}

server {
 access_log /var/log/nginx/access.log combined;
 access_log $blocked_log combined;
}

Here requests returning 444 are written to blocked.log, others to access.log. This way you can track which IPs are being blocked and how much.

At FUTIA, we used this method for italyanmutfagi.com. There are 618 automatically generated recipe pages, bots constantly scan them. We logged 444s separately, dropped from 2-3 GB daily logs to 200 MB. Monthly log storage cost decreased by 85%.

Real Case: 79,000 Profiles and Bot Traffic on doktorbul.com

doktorbul.com is a platform with 79,000 doctor profiles. Created with programmatic SEO, each profile is a separate URL. Bots constantly send requests to these pages, some legitimate (Google, Bing), some scrapers. At one point, 200 requests per second were coming, server CPU was hitting 90%.

Solution: I set up a three-layer block system at the Nginx level.

1. User agent-based block: Cut tools like curl, wget, python-requests with 444. Whitelisted legitimate bots. 2. Rate limiting: 10 requests per second limit per IP, those exceeding get 444. 3. Geo-blocking: Cut traffic from outside Turkey that isn't a legitimate bot with 444.

Result: Server CPU usage dropped from 90% to 35%. Monthly bandwidth went from 1.2 TB to 400 GB. Access log size decreased by 70%. There are also rate limit rules on Cloudflare, but the 444 blocks at the Nginx level had the biggest impact. Because you're cutting traffic that passes through Cloudflare before it even reaches the server.

One more detail: I didn't use access_log off; when returning 444. Because I wanted to see which IPs were being blocked and how much. The first week, the daily log file was 5 GB, then I manually added the 20 IPs sending the most requests to the deny list. The second week, log size dropped to 800 MB.

Advanced: Dynamic 444 Blocks with Lua

With Nginx's Lua module (OpenResty), you can create more complex blocks. For example, you can check an IP list stored in Redis and dynamically return 444:

location / {
 access_by_lua_block {
 local redis = require "resty.redis"
 local red = redis:new()
 red:set_timeout(1000)

 local ok, err = red:connect("127.0.0.1", 6379)
 if not ok then
 ngx.log(ngx.ERR, "Redis connection error: ", err)
 return
 end

 local ip = ngx.var.remote_addr
 local blocked, err = red:get("blocked:".. ip)

 if blocked == "1" then
 ngx.exit(444)
 end
 }

 proxy_pass http://backend;
}

This config asks Redis every time a request comes: "Is this IP blocked?" If it's blocked, it returns 444. You can use a separate script to add IPs to Redis:

redis-cli SET blocked:192.168.1.50 1 EX 3600

This command blocks the IP 192.168.1.50 for 1 hour (3600 seconds). When the time expires, Redis automatically deletes it, the IP can access again.

With Lua, you can also check the request body. For example, you can detect SQL injection attempts and return 444:

access_by_lua_block {
 local args = ngx.req.get_uri_args()
 for key, val in pairs(args) do
 if type(val) == "string" then
 if string.match(val, "SELECT.*FROM") or string.match(val, "UNION.*SELECT") then
 ngx.log(ngx.WARN, "SQL injection attempt: ", ngx.var.remote_addr)
 ngx.exit(444)
 end
 end
 end
}

This code searches for SQL injection patterns like "SELECT FROM" or "UNION SELECT" in URL parameters and returns 444. It writes a warning to the log, records the attacker's IP.

At FUTIA, we did dynamic rate limiting with Lua for futia.net. We kept a counter in Redis for each IP, automatically blocked those exceeding 30 requests per minute for 10 minutes. During 2000+ video productions in 3 months, bots constantly tried, none succeeded.

Performance Measurement: Impact of 444 Blocks

To measure whether 444 blocks actually work, you need to track several metrics:

1. Server CPU and RAM usage: Monitor with htop or Grafana. After 444 blocks are activated, you should see a 50%+ drop in CPU. 2. Bandwidth usage: Measure with Nginx status module or Cloudflare analytics. Bandwidth should decrease by 40-60% with 444 blocks. 3. Access log size: Check with ls -lh /var/log/nginx/access.log. If you're not logging 444s, size should decrease by 70%+. 4. Response time: Average response time should decrease as the number of requests reaching the backend decreases.

Example measurement: diolivo.com.tr before and after 444 blocks

Before (1-week average):

  • Server CPU: 68%
  • Bandwidth: 180 GB/week
  • Access log: 12 GB/week
  • Average response time: 420ms

After (1-week average):

  • Server CPU: 31%
  • Bandwidth: 65 GB/week
  • Access log: 2.8 GB/week
  • Average response time: 280ms

Percentage change:

  • CPU: 54% decrease
  • Bandwidth: 64% decrease
  • Log size: 77% decrease
  • Response time: 33% improvement

These numbers are real. 444 blocks played a critical role in diolivo.com.tr's 340% 6-month traffic growth. Because we allocated server resources to legitimate users, not bots.

Things to Watch Out For and Common Mistakes

You can fall into several traps when using 444:

1. Blocking legitimate bots: Don't forget to whitelist bots from search engines like Google, Bing, Yandex. Otherwise SEO gets reset. 2. Real IP behind CDN or proxy: If you're using a CDN like Cloudflare, Fastly, $remote_addr shows the CDN's IP. You need to use $http_x_forwarded_for or $http_cf_connecting_ip for the real IP. 3. if directive performance issue: Using if in Nginx is not recommended, map or geo is preferred. But if works in simple scenarios. 4. Rate limiting zone size: limit_req_zone 10m might be insufficient. If there are too many IPs, use 50m or 100m. 5. Backend might have received request when returning 444: If you're not returning 444 before proxy_pass, the backend has already taken the load. Put blocks at the top, at the beginning of the server or location block.

There's also a legal situation: Geo-blocking is not legal in some countries. In the EU, you can't block users without informing them under GDPR. There's no such restriction in Turkey, but if you have a global service, get legal advice.

Nginx Security Infrastructure at FUTIA

At FUTIA, we set up security layers at the Nginx level for every project. 444 blocks are a standard part. A typical setup:

  • User agent-based block: curl, wget, scrapy are blocked.
  • Rate limiting: 5-10 requests per second for API endpoints, 50 requests for static files.
  • Geo-blocking: If the project is Turkey-specific, non-Turkey traffic gets 444.
  • Access log filtering: 444s go to a separate file, main log stays clean.
  • Dynamic blocks with Lua: IP blacklist in Redis, automatic blocking.

Example project: kamupersonelhaber.com. 50+ daily job postings, pulled from ilan.gov.tr API. Content produced with Claude Haiku automation. Bots constantly scan the API endpoint. With rate limiting + 444 blocks at the Nginx level, we reduced 200 requests per second to 10 requests. API backend relaxed, response time improved by 60%.

Another example: italyanmutfagi.com. 618 automatic recipes, structured with Schema.org Recipe. Special rate limit for Google bot and other legitimate bots, all other bots get 444. Result: 0 crawl errors in Google Search Console, all pages indexed within 48 hours.

Setting up this kind of security infrastructure requires technical knowledge. You make an Nginx config mistake, the site goes down. As FUTIA, we offer site + automation + monthly maintenance services from the Netherlands to Turkish brands. Security infrastructure comes in the package. If you think your existing site is slowing down due to bot traffic, you can talk to me. Email info@futia.net. With my 6 years of social media marketing and 2 years of AI automation experience, I can set up a similar system for you too.

Frequently Asked Questions

What does Nginx 444 status code do?

Nginx 444 status code instantly closes the TCP connection without returning an HTTP response. Standard HTTP codes (200, 404, 500) send headers and body, giving information to the attacker. 444 sends no data, the attacker can't understand what happened. It's effective against bot attacks, scraping, and DDoS. It can reduce server CPU and bandwidth usage by 50-80%. It should only be applied to malicious traffic, 444 should not be returned to legitimate users.

What's the difference between 444 and 403 or 429?

403 Forbidden tells the user "you don't have access permission" and returns an HTTP response. 429 Too Many Requests says "you exceeded the rate limit" and tells when to retry with the Retry-After header. Both give information to the attacker. 444 returns no response, silently closes the connection. The attacker is left in the dark, can't understand what happened. That's why 444 is more effective against bot and DDoS attacks. But if you return 444 to legitimate users, they'll see an ambiguous error message in the browser, so it should be used carefully.

Are Nginx 444 blocks written to access log?

By default, 444 status code is also written to access log. But if there are thousands of bot requests, the log file gets very large, disk I/O increases. To not log 444s, you can use the map directive: map $status $loggable { 444 0; default 1; } and configure it as access_log combined if=$loggable;. Alternatively, you can write 444s to a separate log file, so you can track which IPs are being blocked and how much. In FUTIA projects, we log 444s separately, the main log stays clean, analysis is easy.

How is rate limiting combined with 444?

You set a rate limit with Nginx's limit_req module, and return 444 to requests exceeding the limit with the limit_req_status 444; directive. Example: limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s; and configure it as limit_req_status 444;. There will be a 5 requests per second limit for each IP, those exceeding get 444. Note: limit_req_status is a global directive, it applies to all locations. If you want to return 444 only to specific endpoints, you need to use different zones. At FUTIA, we apply low rate limits for API endpoints, high rate limits for static files.

How do we protect legitimate bots (Google, Bing) from 444?

When doing user agent-based blocking, you need to whitelist legitimate bots. You do it like this with the map directive: map $http_user_agent $block_ua { default 0; ~*Googlebot 0; ~*Bingbot 0; ~*baiduspider 0; ~*YandexBot 0; ~*curl 1; ~*wget 1; }. Here bots like Googlebot, Bingbot get a value of 0 (not blocked), tools like curl and wget get a value of 1 (blocked). Note: User agent can be fake, an attacker can write "Googlebot". A more secure method is IP-based whitelist, you whitelist Google's bot IP ranges with the geo directive.

ABOUT THE AUTHOR
Miraç Eroğlu

Hacettepe mezunu, 6 yıldır sosyal medya, 2 yıldır AI otomasyon.

Learn more →

Want to apply one of the techniques from this post? Fill out a short form and we'll email you a free preview audit within 48 hours.