Tricky Apache & mod_wsgi Issues I’ve Run Into (So You Don’t Have To)

Mostafa-DEMostafa-DE
8 min read

I always say this (Thank god we have default values). They save thousands of developers every day. Imagine if we didn’t have default values that were set based on the common cases, we would face a really hard time doing everything over and over.

But not for everything. In my opinion, you should never trust default values. Notice that I didn’t say don't use them; I said don't trust them. You may still use default values, but never blindly trust them. One day, they will fail you if you don't properly understand what you are using.

I've been there and I saw productions used default values for most of the things, but also I saw these values most of the time fail. This is really important to understand, not only for this article topic, but is beyond that. You should never trust default values, especially in the DevOps world, you should always understand what you are configuring and put it under proper testing, otherwise you are just taking unnecessary risks.

With that said, let’s start on our topic for this article

Running mod_wsgi in daemon mode with MPM Event is a great setup, but it’s also easy to misconfigure Apache in ways that tank performance, create stability issues, or even break your application. A lot of these issues come from poorly tuned process/thread settings, misaligned worker configurations, or because you are using the default values.

This article goes over real-world examples of bad configurations, why they fail, and how to properly configure Apache and mod_wsgi for efficient production workloads.

Common Misconfigurations & How to Fix Them

Misaligned StartServers, MaxRequestWorkers, ThreadsPerChild Underutilisation of Resources

A common mistake is setting MaxRequestWorkers too low compared to available worker processes and threads, leaving potential performance on the table.

<IfModule mpm_event_module>
    StartServers 3
    MinSpareThreads 25
    MaxSpareThreads 75
    ThreadsPerChild 50
    MaxRequestWorkers 75
</IfModule>

WSGIDaemonProcess myapp processes=4 threads=25
WSGIProcessGroup myapp

What's Wrong?

  • StartServers used to tell Apache how many workers to create when it first starts, it is good for making sure that it can handle a lot of requests when it starts

  • ThreadsPerChild used to specify how many threads each worker can use to handle requests

    • In simple terms, you can say that each thread eventually will handle one request at a time
  • MinSpareThreads and MaxSpareThreads used to specify the min and max of how many threads should be created by Apache and kept for future use.

  • MaxRequestsWorkers The most important config that you need to be careful with once you set, it is used to tell Apache the maximum number of requests it can serve at the same time

    • This is used to limit Apache from utilizing the entire resources

    • When I say (be careful) when setting this, I mean it, and here is why?

When you set StartServers to a specific value, Apache will start creating workers, each worker will start creating threads based on the specified value, but the problem here when you set MaxRequestsWorkers you basically telling Apache how many requests it can handle at once, so if you set it a low number, Apache will see that there are 3 workers and each one can handle 50 requests at a time, so the total is 3 × 50 = 150 which means when you set 75 as a maximum, it will start killing threads to reach the limit that you specified which will defiantly will cause problems and issues like (misutilisation, Zombie processes) and so on, so be careful.

MaxRequestWorkers (75) too low. MPM Event can handle 3 processes × 50 threads = 150 requests, but it's capped at 75.

Wasted capacity: Apache can handle more concurrent requests, but it's artificially restricted.

→ To Fix This: Align Settings for Proper Utilization

<IfModule mpm_event_module>
    StartServers 3
    MinSpareThreads 25
    MaxSpareThreads 75
    ThreadsPerChild 50
    MaxRequestWorkers 150  # Matches 3 processes × 50 threads
</IfModule>

WSGIDaemonProcess myapp processes=3 threads=50  # Matches MPM Event
WSGIProcessGroup myapp

Not Setting --processes For mod_wsgi, blocking Requests Due to Slow Processing

Running Apache in Daemon mode means it acts as a proxy, which means the requests are forwarded to WSGI to be handled on the application level, so imagine you have a slow application with huge requests to process, that means you have slowness, requests being dropped, and in most cases outage.

One way to mitigate this is to increase the number of servers to handle requests, but that’s not helpful if your server is not utilised 100%. Imagine this scenario:

  • You have a server with 4 cores and 16 RAM, this server is considered mid-level, which is really good for most applications

    • But if you have 1 application running on that server and that is processing everything coming from Apache, at some point, things will get delayed, and your application can’t follow up with Apache

    • In cases like this, you can just add another server to help, but what about the cost? Why pay money for something that you can easily avoid? You are not fully utilising the server that you already have, so instead of adding servers, why not try to fully utilise what we have instead

  • This is my argument, you should always try to figure out a way to get use of what you already have instead of introducing something new to the equation.


Missing or incorrect listen-backlog, too Many Dropped Requests

By default, mod_wsgi sets listen-backlog to 128 by default, which means dropped connections during traffic spikes. I saw many production configs not using this, although to be honest it is not that big of deal, the default is already 128 which is fine in most cases specially if you production uses 2 servers that are running all the time, but in case where you app is running on only one server I would recommend to increase it to a higher number. I won’t know the exact number to set, as always, this depends on your app and resources and so on. With that said, I would recommend not setting a really high number as this could make things worst during spikes if your application is slow, so find what number work with your app.

→ To Fix: Increase listen-backlog

WSGIDaemonProcess myapp processes=4 threads=25 listen-backlog=500
WSGIProcessGroup myapp
  • Setting listen-backlog to 500 ensures that requests are queued and aren’t rejected immediately, which helps your app reputation (No one likes to see 502/503 errors or something similar once hitting any application).

mod_remoteip can't parse Client IP

When using Apache behind an AWS ALB, the X-Forwarded-For header forwarded by the ALB includes both the IP and the port (e.g., 203.0.113.1:54321). This causes the mod_remoteip module to fail when trying to parse the real client IP.

What's Wrong?

  • mod_remoteip expects just the IP (without the port). When it can't parse the header, it defaults to logging and treating the request as if it came from the ALB IP.

  • All requests appear to originate from the load balancer, which completely breaks IP-based logic.

And of course, this will lead to:

  • Incorrect logging: All client logs will show the ALB IP, not the actual user IP.

  • Broken rate limiting: Tools like mod_evasive or WAFs that rely on accurate IP tracking will see all requests coming from one source (the ALB) and could start blocking legitimate users.

→ To Fix: Prevent ALB from Appending Port

Modify the ALB configuration and disable port forwarding:

enable_xff_client_port = false

This ensures X-Forwarded-For contains only the IP. Once the ALB is fixed, make sure Apache knows how to extract the real IP:

RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy <ALB_IP/CIDR>

This way, Apache replaces the client IP with the value from X-Forwarded-For, but only if it came from your trusted ALB. This restores proper logging, WAF accuracy, and rate limiting logic.


mod_remoteip and Multi-Proxy Setups: How Misconfiguration Can Expose You to IP Spoofing

When using mod_remoteip with reverse proxies like an ALB, Cloudflare, or NGINX, many setups mistakenly trust whatever is in the X-Forwarded-For header, without validating who sent it. This makes it easy for attackers to spoof IPs.

What's Wrong?

  • X-Forwarded-For can contain multiple IPs (e.g. client, proxy1, proxy2).

  • If Apache isn’t explicitly told which proxy IPs it should trust, it may trust attacker-controlled headers.

  • That means your logs, WAF rules, rate limiters (e.g., mod_evasive), and even audit trails might all log fake IPs.

→ To Fix: Define Who You Actually Trust

1. Use RemoteIPHeader and RemoteIPInternalProxy properly

RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 10.0.0.0/8 192.168.0.0/16  # Only trust your internal proxy network
  • This tells Apache: “Only trust this header if the request came from one of these trusted internal proxies.”

  • If you're using an ALB, make sure to list its private IP or CIDR block.

  • If you're behind multiple layers (e.g., Cloudflare → ALB → Apache), configure this carefully.


Bonus Tip: Use Custom Logging Formats

You’re not stuck with the defaults. You can actually define your own log formats and log extra info that’s helpful when debugging.

LogFormat "%h %l %u %t \"%r\" %>s %b %{ms}T" custom
CustomLog /var/log/apache2/access.log custom
  • In the example above, %{ms}T logs the request duration in milliseconds—super helpful when tracking performance issues.

  • You can log headers, cookies, query strings—whatever helps you trace problems.

That's all for now. If you're interested in more advanced content about WSGI, Apache, or other DevOps topics, there are many articles in the DevOps series that you might find interesting.

Mostafa-DE Fayyad

Software Engineer

0
Subscribe to my newsletter

Read articles from Mostafa-DE directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mostafa-DE
Mostafa-DE