Fixing Django slow response time and 300ms latency on localhost

In my previous guide, Deploying Django on Windows: A Complete Guide using Nginx, Waitress, and PostgreSQL, I detailed the exact stack I use for my local projects. However, while playing around with this setup on a new small project with zero traffic, I noticed something deeply frustrating, a response time of 300-400ms for a simple Django JSON response and around 700ms for regular HTML pages.

Even though my database was small—with only about 500 entries—and there was absolutely no load on the system, the latency remained stubbornly high. As I am not a Django expert yet, I started questioning everything. I searched for all sorts of solutions, diving deep into middleware configurations and database optimizations, but I couldn’t understand where this lag was coming from. My code was minimal, the data was light, and yet every single request felt like it was stuck in traffic.

It felt like there was a “ghost” in my system, holding back every packet of data. After hours of digging, I realized the problem wasn’t in my Python code or my Django settings, it was something much closer to the operating system, quietly scanning every move I made.

To isolate the bottleneck, I methodically tested every single component of my stack, running SQL queries directly in pgAdmin 4 (which took only 7ms), swapping the development server for Waitress, and even configuring Nginx on Windows, yet surprisingly, every individual part performed perfectly and none of them could explain where that massive latency was coming from.

Since none of my local components were at fault, I started looking at the system-level background processes. Being on a company laptop where I didn’t have full administrative control, I reached out to the IT Department with a specific request: “Can we temporarily disable the Bitdefender antivirus to test a theory?”

As soon as they turned off the real-time scanning, the results were night and day. Surprise! My response times instantly dropped from 300ms to a crisp 50ms.

It turned out that Bitdefender’s protection was acting as a “Man-in-the-Middle,” intercepting and scanning every single data packet traveling between my local Django server and my browser. Even for a tiny project with 500 database entries, the “security tax” was over 300ms per request. I had finally found the culprit.

Even though my project was strictly internal, running only within our local network, the IT Department had strict security policies. I wasn’t allowed to add exceptions for Python, SQL processes, or specific local ports. If you find yourself in this “security vs. performance” deadlock, here is how you can handle it:

In settings.py from my django app i set CONN_MAX_AGE:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mypatabase',
        # ... other settings
        'CONN_MAX_AGE': None,  # Keep the connection open indefinitely
    }
}

By setting CONN_MAX_AGE: None, Django maintains a persistent connection to the database. Instead of the antivirus having to “authorize” a new connection for every single page load, the pipe stays open. This reduced the “chatter” between my app and PostgreSQL, saving me precious milliseconds that Bitdefender would have otherwise intercepted.

Since I was stuck with the antivirus “tax,” I had to make sure my server gateway, Waitress, was configured to be as resilient as possible. I didn’t want the server to drop connections or time out, which would force new, slow handshakes.

In my run_server.py (or wherever you trigger the server), I applied these specific settings:

serve(
    application,
    host='127.0.0.1',
    port=8080,
    threads=4,              # Balanced for a small project to handle concurrent requests
    connection_limit=200,   # Ensures we can handle many idle/open connections
    channel_timeout=3600,   # The Key to Success: Keeps the channel open for an hour
    cleanup_interval=30,    # Low interval to clear errors without affecting speed
    backlog=2048,           # Large queue to handle bursts of traffic
)

Why this matters:

  • channel_timeout=3600: This is the secret sauce. By keeping the channel open for a long duration, you prevent the socket from closing prematurely. This minimizes the frequency of the antivirus having to “inspect” a brand-new connection sequence.
  • threads=4: For a small project, you don’t need hundreds of threads. Keeping it at 4 ensures the CPU isn’t wasted on context switching while still allowing parallel processing of requests.
  • backlog=2048: This ensures that even if Bitdefender slows down a few requests, the server can still queue up others without dropping them.

My core idea was simple: if the antivirus scans every single TCP handshake, then the solution isn’t to add more threads, but to limit them and force their reuse.

In a small project like this, I don’t need dozens of processes constantly opening and closing. Every new connection is just another invitation for Bitdefender to step in and add that 300ms “inspection tax.”

By configuring Waitress with a long channel_timeout and a stable thread pool, I forced the system to keep the communication “pipe” open. Once the antivirus scans the initial connection, the subsequent data flows through the already-established channel much faster. I stopped the “chatter” between the server and the browser, effectively tricking the real-time scanning engine into letting my local traffic pass without constant re-interruption.

The Result: A “Warm-Up” Period

There is one important thing to notice when using this setup: the first few requests will still be slow. Because we are limiting the server to a small pool of 4 threads, the antivirus will still scan the very first handshake for each of those threads. This means your first 4 requests might still show that 300-400ms latency.

However, once those initial connections are established and the “pipes” are open, the magic happens. Every subsequent request will bypass the handshake inspection, and you will see your response times drop from 300ms to a crisp 50ms. You are essentially “warming up” the server to maintain a fast, persistent flow.

Conclusion

The biggest takeaway from this experience on Playground is that performance isn’t just about writing clean Python code, it’s about understanding the entire ecosystem your code lives in.

Even with a small database of 500 entries and optimized settings like CONN_MAX_AGE, a security policy can still artificially inflate your response times. If you see a consistent 300ms delay, stop refactoring your views and start looking at your background processes. Sometimes, the “slowest” part of your Django app isn’t Django at all—it’s the software meant to protect it.

Final Warning & Disclaimer

Please keep in mind that the configurations and workarounds shared here are part of my personal journey to learn and experiment on Playground. While these settings successfully solved my latency issues in a local development environment, they should be approached with caution.

I am not a security or infrastructure expert—I am testing these limits out of a desire to learn how systems interact. Before applying these settings to a production environment or a sensitive network, I strongly recommend that you:

  • Study each component individually (Django, Waitress, and your Security Software).
  • Consult official documentation for the recommended best practices.
  • Understand the security trade-offs of bypassing or persistent-loading network traffic.

Every project is unique, so use these tips as a starting point for your own research, not as a universal rule!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *