Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Low RPS when perfomance testings django website

I have a code like this that caches a page for 60 minutes:

import os
import time
from django.conf import settings
from django.core.cache import cache
from django.core.mail import send_mail
from django.contrib import messages
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import render
from django.utils.translation import get_language, gettext as _

from apps.newProduct.models import Product, Variants, Category
from apps.vendor.models import UserWishList, Vendor
from apps.ordering.models import ShopCart
from apps.blog.models import Post
from apps.cart.cart import Cart

# Cache timeout for common data
CACHE_TIMEOUT_COMMON = 900  # 15 minutes

def cache_anonymous_page(timeout=CACHE_TIMEOUT_COMMON):
    from functools import wraps
    from django.utils.cache import _generate_cache_header_key

    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kw):
            if request.user.is_authenticated:
                return view(request, *args, **kw)

            lang = get_language()                          # i18n
            curr = request.session.get('currency', '')
            country = request.session.get('country', '')
            cache_key = f"{view.__module__}.{view.__name__}:{lang}:{curr}:{country}"

            resp = cache.get(cache_key)
            if resp is not None:
                return HttpResponse(resp)

            response = view(request, *args, **kw)
            if response.status_code == 200:
                cache.set(cache_key, response.content, timeout)
            return response
        return wrapper
    return decorator


def get_cached_products(cache_key, queryset, timeout=CACHE_TIMEOUT_COMMON):
    lang = get_language()
    full_key = f"{cache_key}:{lang}"
    data = cache.get(full_key)
    if data is None:
        data = list(queryset)
        cache.set(full_key, data, timeout)
    return data

def get_cached_product_variants(product_list, cache_key='product_variants', timeout=CACHE_TIMEOUT_COMMON):
    lang = get_language()
    full_key = f"{cache_key}:{lang}"
    data = cache.get(full_key)
    if data is None:
        data = []
        for product in product_list:
            if product.is_variant:
                data.extend(product.get_variant)
        cache.set(full_key, data, timeout)
    return data

def get_all_cached_data():
    featured_products = get_cached_products(
        'featured_products',
        Product.objects.filter(status=True, visible=True, is_featured=True)
               .exclude(image='')
               .only('id','title','slug','image')[:8]
    )
    popular_products = get_cached_products(
        'popular_products',
        Product.objects.filter(status=True, visible=True)
               .exclude(image='')
               .order_by('-num_visits')
               .only('id','title','slug','image')[:4]
    )
    recently_viewed_products = get_cached_products(
        'recently_viewed_products',
        Product.objects.filter(status=True, visible=True)
               .exclude(image='')
               .order_by('-last_visit')
               .only('id','title','slug','image')[:5]
    )
    variants = get_cached_products(
        'variants',
        Variants.objects.filter(status=True)
                .select_related('product')
                .only('id','product','price','status')
    )
    product_list = get_cached_products(
        'product_list',
        Product.objects.filter(status=True, visible=True)
               .prefetch_related('product_variant')
    )
    return featured_products, popular_products, recently_viewed_products, variants, product_list

def get_cart_info(user, request):
    if user.is_anonymous:
        return {}, 0, [], 0, []
    cart = Cart(request)
    wishlist = list(UserWishList.objects.filter(user=user).select_related('product'))
    shopcart_qs = ShopCart.objects.filter(user=user).select_related('product','variant')
    shopcart = list(shopcart_qs)
    products_in_cart = [item.product.id for item in shopcart if item.product]
    total = cart.get_cart_cost()
    comparing = len(request.session.get('comparing', []))
    compare_var = len(request.session.get('comparing_variants', []))
    total_compare = comparing + compare_var
    if len(cart) == 0:
        shopcart = []
    return {
        'cart': cart,
        'wishlist': wishlist,
        'shopcart': shopcart,
        'products_in_cart': products_in_cart,
    }, total, wishlist, total_compare, shopcart

@cache_anonymous_page(3600)
def frontpage(request):
    featured_products, popular_products, recently_viewed_products, variants, product_list = get_all_cached_data()
    var = get_cached_product_variants(product_list)
    cart_ctx, total, wishlist, total_compare, shopcart = get_cart_info(request.user, request)
    context = {
        'featured_products': featured_products,
        'popular_products': popular_products,
        'recently_viewed_products': recently_viewed_products,
        'variants': variants,
        'var': var,
        **cart_ctx,
        'subtotal': total,
        'total_compare': total_compare,
    }
    return render(request, 'core/frontpage.html', context)

I installed django debug toolbar and it shows time ~40 ms for a cached frontpage. My server has 2 CPUs. When i try perfomance testing using locust I get around 3 RPS. I thought i would get around 2CPU*(1000/40) ~ 50 RPS.

I run my server using this command inside docker container:

gunicorn main.wsgi:application
            -k gevent
            --workers 6
            --bind 0.0.0.0:8080
            --worker-connections 1000
            --timeout 120

Also i use psycopg2 with psycogreen wsgi.py starts with this:

from psycogreen.gevent import patch_psycopg
patch_psycopg()

What am i doing wrong? Why can't handle more RPS?

  1. I'm using redis like this:

    CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://supapp_redis_1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } } }

  2. i'm trying to test with anonymous users because this is the firstpage every user will see. My locustfile.py look like this:

    from locust import HttpUser, task, between

    class WebsiteUser(HttpUser): def index(self): self.client.get("/")

  3. Gunicorn is using 2 cpus. When i use htop and test with locust i can see 100% usage on both cpus

  4. My testsite.com.conf has these lines:

    Serve static files

    location /static/ { alias /opt/supapp/data/Shop/static/; }

    Serve media files

    location /media/ { alias /opt/supapp/data/Shop/media/; }

I use nginx for static content

Unfortunately i still get 3 RPS with 2 CPUS

PS: I use Django templates for my frontend(SSR)

like image 646
Denzel Avatar asked Nov 30 '25 10:11

Denzel


1 Answers

You're likely hitting these issues:

1. Using LocMemCache (Default):

Each Gunicorn worker has its own cache = no shared cache.

To fix this use Redis or Memcached in settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
    }
}

2. Your Locust Load Test Might Use Logged-in Users

Your cache only works for anonymous users:

if request.user.is_authenticated:
    return view(request, *args, **kw)

Load test with anonymous (non-authenticated) users.

3. Gunicorn Not Using All CPUs in Docker

Use all cores dynamically:

--workers $(nproc)

Full command:

gunicorn main.wsgi:application \
    -k gevent \
    --workers $(nproc) \
    --worker-connections 1000 \
    --timeout 120 \
    --bind 0.0.0.0:8080

Also, ensure Docker is allowed to use 2 CPUs:

docker run --cpus="2.0" ...

4. Gunicorn is slow for static files.

Use Nginx or a CDN for static files.

AS you still get 4 RPS, Now try below changes might help:

  1. Update your cache_anonymous_page to Cache the entire HttpResponse object, not just .content
def cache_anonymous_page(timeout=CACHE_TIMEOUT_COMMON):
    from functools import wraps
    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kw):
            if request.user.is_authenticated:
                return view(request, *args, **kw)

            lang = get_language()
            curr = request.session.get('currency', '')
            country = request.session.get('country', '')
            cache_key = f"{view.__module__}.{view.__name__}:{lang}:{curr}:{country}"

            cached_resp = cache.get(cache_key)
            if cached_resp:
                return cached_resp  # full HttpResponse object

            response = view(request, *args, **kw)
            if response.status_code == 200:
                cache.set(cache_key, response, timeout)
            return response
        return wrapper
    return decorator

2. Try Using threads or processes instead of gevent

gunicorn main.wsgi:application --workers 2 --threads 10 --timeout 120 --bind 0.0.0.0:8080

3. Check if template rendering is the problem:
Try returning a hard-coded HTML string:

from django.http import HttpResponse

def cached_healthcheck(request):
    return HttpResponse("<html><body><h1>ok</h1></body></html>")

Benchmark it with wrk or ab:

ab -n 1000 -c 100 http://localhost:8080/healthcheck/

f that gives you 1000+ RPS, but your frontpage gives 4 RPS then template rendering is the problem.

4. Try running container like:

docker run --cpus=2.0 --memory="2g" ...

5.Try below, If RPS doesn’t increase with higher concurrency then server is bottleneck. If it does, your Locust setup was too small.

locust -u 100 -r 20 --headless -t 1m -H http://yourserver

Also try using wrk:

wrk -t10 -c100 -d30s http://localhost:8080/
like image 121
amrita yadav Avatar answered Dec 02 '25 22:12

amrita yadav



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!