LoadForge LogoLoadForge

Drupal Load Testing: How to Stress Test Drupal Sites with LoadForge

Drupal Load Testing: How to Stress Test Drupal Sites with LoadForge

Introduction

Drupal load testing is essential if you want your site to stay fast, stable, and usable during traffic spikes, marketing campaigns, product launches, or seasonal demand. Whether you run a content-heavy Drupal CMS, a membership portal, or a Drupal Commerce storefront, performance problems often appear only when real concurrency hits the application stack.

A Drupal site can look perfectly healthy with a handful of users, then slow down dramatically once PHP workers are saturated, cache hit rates drop, database queries pile up, or authenticated traffic bypasses page caching. That is exactly why load testing, performance testing, and stress testing matter for Drupal.

In this guide, you’ll learn how to load test Drupal sites with LoadForge using realistic Locust scripts. We’ll cover anonymous browsing, authenticated user flows, JSON:API requests, Drupal Commerce cart actions, and form submissions. Along the way, we’ll show how to simulate real traffic patterns so you can uncover bottlenecks before your users do.

LoadForge makes this process easier with cloud-based infrastructure, distributed testing, real-time reporting, CI/CD integration, and global test locations—so you can test Drupal performance at scale without managing your own load generators.

Prerequisites

Before you start load testing Drupal with LoadForge, make sure you have the following:

  • A working Drupal environment:
    • Drupal 9, 10, or later
    • Production-like infrastructure if possible
    • Access to staging or pre-production preferred
  • A list of realistic user journeys, such as:
    • Homepage visits
    • Category or taxonomy browsing
    • Node/article views
    • Search
    • User login
    • Add-to-cart and checkout steps for Drupal Commerce
    • JSON:API or REST API calls
  • Test accounts for authenticated scenarios
  • If using CSRF-protected forms or APIs, access to the required tokens
  • If using Drupal Commerce:
    • Product variation IDs or purchasable entity IDs
    • Cart endpoints or storefront page URLs
  • If using JSON:API:
    • Enabled JSON:API module
    • Known resource paths such as /jsonapi/node/article
  • A LoadForge account to run distributed load tests and analyze results

You should also understand your Drupal caching layers before testing:

  • Drupal page cache
  • Dynamic Page Cache
  • Reverse proxy cache such as Varnish or Cloudflare
  • CDN behavior
  • PHP-FPM worker limits
  • Database backend performance
  • Redis or Memcached if used for cache bins
  • Search backend like Solr or Elasticsearch if applicable

The more closely your test environment matches production, the more useful your Drupal load testing results will be.

Understanding Drupal Under Load

Drupal performance depends heavily on the type of traffic your site receives.

Anonymous traffic

Anonymous users often benefit from the strongest caching. For content pages, Drupal can serve cached responses quickly through:

  • CDN edge cache
  • Reverse proxy cache
  • Internal page cache

This means anonymous page views may scale well until cache invalidation frequency rises or uncached endpoints become hot spots.

Authenticated traffic

Authenticated traffic is where many Drupal sites struggle. Logged-in users often bypass full-page caching and trigger:

  • Session handling
  • Personalized content rendering
  • Permission checks
  • More database queries
  • Extra backend processing

Membership sites, editorial dashboards, and e-commerce stores commonly experience these issues.

Drupal Commerce workloads

Drupal Commerce introduces additional complexity:

  • Cart creation and updates
  • Price calculations
  • Promotions and tax rules
  • Inventory checks
  • Checkout workflows
  • Payment gateway interactions

These actions are usually dynamic and database-heavy, making them prime targets for performance testing and stress testing.

API-driven Drupal sites

Headless Drupal or decoupled front ends often depend on JSON:API or REST endpoints. Under load, bottlenecks may appear in:

  • Entity serialization
  • Filtering and sorting queries
  • Large relationship expansions
  • Authentication and token validation
  • Cache misses for API responses

Common Drupal bottlenecks

When load testing Drupal, watch for these frequent problem areas:

  • Slow uncached node or taxonomy pages
  • Search pages with expensive queries
  • Login and session contention
  • High CPU usage in PHP-FPM
  • Database locks or slow query growth
  • Cache stampedes after invalidation
  • Media-heavy pages with slow asset delivery
  • Cron jobs or queue workers competing for resources
  • Third-party modules introducing expensive hooks or event subscribers

A good Drupal performance testing strategy should include both cached and uncached scenarios to reflect real user behavior.

Writing Your First Load Test

Let’s start with a realistic anonymous browsing test for a Drupal site. This scenario simulates users visiting the homepage, browsing article listings, viewing taxonomy pages, and opening content nodes.

Basic anonymous Drupal browsing test

python
from locust import HttpUser, task, between
 
class DrupalAnonymousUser(HttpUser):
    wait_time = between(1, 5)
 
    @task(4)
    def homepage(self):
        self.client.get("/", name="GET /")
 
    @task(3)
    def article_listing(self):
        self.client.get("/news", name="GET /news")
 
    @task(2)
    def taxonomy_page(self):
        self.client.get("/tags/performance", name="GET /tags/performance")
 
    @task(5)
    def article_page(self):
        article_paths = [
            "/news/how-to-scale-drupal-for-high-traffic-events",
            "/blog/drupal-caching-best-practices",
            "/resources/drupal-commerce-performance-checklist"
        ]
        for path in article_paths:
            self.client.get(path, name="GET content page")

What this script does

This first Locust script models anonymous traffic patterns common on Drupal CMS sites:

  • Homepage requests
  • Listing pages
  • Taxonomy term pages
  • Individual node pages

The name parameter groups similar requests in LoadForge reports, making it easier to analyze performance by endpoint type rather than every unique URL.

Why this matters for Drupal

This test helps you answer questions like:

  • Is your reverse proxy or CDN caching effectively?
  • Are node pages staying fast under concurrent traffic?
  • Do taxonomy pages generate expensive queries?
  • Does response time degrade as concurrency increases?

For many Drupal sites, anonymous traffic is the baseline performance layer. If this is already slow, authenticated or commerce traffic will likely be worse.

Advanced Load Testing Scenarios

Once you’ve validated basic anonymous browsing, you should move to more realistic Drupal load testing scenarios.

Authenticated user login and account page testing

Authenticated traffic is critical for Drupal membership sites, editorial portals, intranets, and customer accounts. Drupal login typically uses the /user/login form, which requires a CSRF-related form token and form_build_id values generated by the page.

A realistic test first loads the login form, extracts hidden fields, then submits credentials.

python
import re
from locust import HttpUser, task, between
 
class DrupalAuthenticatedUser(HttpUser):
    wait_time = between(2, 6)
 
    username = "loadtest_user_01"
    password = "StrongPassword123!"
 
    def on_start(self):
        self.login()
 
    def extract_hidden_field(self, html, field_name):
        pattern = rf'name="{field_name}" value="([^"]+)"'
        match = re.search(pattern, html)
        return match.group(1) if match else None
 
    def login(self):
        with self.client.get("/user/login", name="GET /user/login", catch_response=True) as response:
            if response.status_code != 200:
                response.failure("Failed to load login page")
                return
 
            html = response.text
            form_build_id = self.extract_hidden_field(html, "form_build_id")
            form_token = self.extract_hidden_field(html, "form_token")
            form_id = self.extract_hidden_field(html, "form_id")
 
            if not form_build_id or not form_token or not form_id:
                response.failure("Missing Drupal login form fields")
                return
 
        payload = {
            "name": self.username,
            "pass": self.password,
            "form_build_id": form_build_id,
            "form_token": form_token,
            "form_id": form_id,
            "op": "Log in"
        }
 
        with self.client.post(
            "/user/login",
            data=payload,
            name="POST /user/login",
            allow_redirects=True,
            catch_response=True
        ) as login_response:
            if login_response.status_code not in [200, 302]:
                login_response.failure(f"Login failed with status {login_response.status_code}")
            elif "/user/login" in login_response.url:
                login_response.failure("Login appears unsuccessful; still on login page")
 
    @task(3)
    def view_dashboard(self):
        self.client.get("/user", name="GET /user")
 
    @task(2)
    def edit_profile_page(self):
        self.client.get("/user/1/edit", name="GET /user/[id]/edit")
 
    @task(4)
    def browse_member_content(self):
        self.client.get("/members/resources", name="GET /members/resources")

Why this test is useful

This script simulates a real Drupal login flow rather than posting static credentials blindly. That matters because Drupal forms often include dynamic hidden values.

Use this scenario to measure:

  • Login throughput
  • Session handling overhead
  • Authenticated page performance
  • Cache bypass impact
  • Database pressure caused by logged-in users

If login becomes slow under modest concurrency, check session storage, database contention, and PHP worker utilization.

JSON:API load testing for headless Drupal

Many modern Drupal implementations use JSON:API for decoupled front ends. These endpoints can become expensive under load, especially when filtering, sorting, or including related entities.

Here’s a realistic JSON:API load test for a headless Drupal site serving article content.

python
from locust import HttpUser, task, between
 
class DrupalJsonApiUser(HttpUser):
    wait_time = between(1, 3)
 
    headers = {
        "Accept": "application/vnd.api+json"
    }
 
    @task(4)
    def list_articles(self):
        self.client.get(
            "/jsonapi/node/article?page[limit]=10&sort=-created",
            headers=self.headers,
            name="GET /jsonapi/node/article"
        )
 
    @task(2)
    def filtered_articles(self):
        self.client.get(
            "/jsonapi/node/article?filter[status]=1&filter[field_tags.name]=Performance&sort=-created&page[limit]=5",
            headers=self.headers,
            name="GET filtered articles"
        )
 
    @task(3)
    def single_article_with_relationships(self):
        self.client.get(
            "/jsonapi/node/article/2f3d7f17-8e6d-4a25-9f5f-123456789abc?include=field_author,field_tags",
            headers=self.headers,
            name="GET article with includes"
        )
 
    @task(1)
    def taxonomy_terms(self):
        self.client.get(
            "/jsonapi/taxonomy_term/tags?page[limit]=20",
            headers=self.headers,
            name="GET /jsonapi/taxonomy_term/tags"
        )

What to watch in Drupal API tests

JSON:API performance testing often reveals:

  • Slow serialization of entity relationships
  • Poorly indexed filters
  • Large payloads
  • Cache misses on API endpoints
  • Excessive backend work for includes and nested resources

In LoadForge, compare the response times of simple list endpoints versus filtered or relationship-heavy requests. Large differences often point to query optimization or API design issues.

Drupal Commerce add-to-cart and checkout flow testing

If you run Drupal Commerce, you need to test more than page views. Cart and checkout workflows are often the most business-critical and the most performance-sensitive.

The example below simulates a shopper browsing a product page, adding an item to cart, viewing the cart, and entering checkout.

python
import re
from locust import HttpUser, task, between, SequentialTaskSet
 
class CommerceFlow(SequentialTaskSet):
    def extract_hidden_field(self, html, field_name):
        pattern = rf'name="{field_name}" value="([^"]+)"'
        match = re.search(pattern, html)
        return match.group(1) if match else None
 
    @task
    def view_product(self):
        with self.client.get(
            "/products/drupal-performance-t-shirt",
            name="GET product page",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure("Product page failed")
                return
 
            html = response.text
            self.form_build_id = self.extract_hidden_field(html, "form_build_id")
            self.form_token = self.extract_hidden_field(html, "form_token")
            self.form_id = self.extract_hidden_field(html, "form_id")
 
            if not self.form_build_id or not self.form_token or not self.form_id:
                response.failure("Missing add-to-cart form fields")
 
    @task
    def add_to_cart(self):
        payload = {
            "purchased_entity[0][variation]": "17",
            "quantity[0][value]": "1",
            "form_build_id": self.form_build_id,
            "form_token": self.form_token,
            "form_id": self.form_id,
            "op": "Add to cart"
        }
 
        with self.client.post(
            "/products/drupal-performance-t-shirt",
            data=payload,
            name="POST add to cart",
            allow_redirects=True,
            catch_response=True
        ) as response:
            if response.status_code not in [200, 302]:
                response.failure(f"Add to cart failed: {response.status_code}")
 
    @task
    def view_cart(self):
        self.client.get("/cart", name="GET /cart")
 
    @task
    def begin_checkout(self):
        self.client.get("/checkout/1", name="GET /checkout/[order_id]")
 
class DrupalCommerceUser(HttpUser):
    wait_time = between(3, 7)
    tasks = [CommerceFlow]

Notes on Drupal Commerce realism

This example reflects how Drupal Commerce often works in practice:

  • Product pages contain dynamic form fields
  • Add-to-cart submits a variation ID
  • Cart and checkout are stateful
  • Session persistence matters

In your own site, adjust:

  • Product paths
  • Variation field names
  • Checkout URL patterns
  • Cart behavior
  • Any CSRF or custom module requirements

This kind of performance testing is valuable because it exercises the dynamic parts of Drupal that caching cannot easily hide.

Drupal form submission and search testing

Another common bottleneck in Drupal is search and form handling. Search pages can be database-heavy or search-backend-heavy, and forms often involve validation, anti-spam modules, and email hooks.

python
import re
from locust import HttpUser, task, between
 
class DrupalSearchAndFormsUser(HttpUser):
    wait_time = between(2, 5)
 
    @task(4)
    def site_search(self):
        self.client.get(
            "/search/node?keys=drupal+performance",
            name="GET site search"
        )
 
    @task(2)
    def advanced_search(self):
        self.client.get(
            "/search/node?keys=commerce&f%5B0%5D=type%3Aproduct",
            name="GET filtered search"
        )
 
    @task(1)
    def contact_form_submission(self):
        with self.client.get("/contact", name="GET /contact", catch_response=True) as response:
            if response.status_code != 200:
                response.failure("Failed to load contact form")
                return
 
            html = response.text
            form_build_id = re.search(r'name="form_build_id" value="([^"]+)"', html)
            form_token = re.search(r'name="form_token" value="([^"]+)"', html)
            form_id = re.search(r'name="form_id" value="([^"]+)"', html)
 
            if not form_build_id or not form_token or not form_id:
                response.failure("Missing contact form fields")
                return
 
        payload = {
            "name": "Load Test User",
            "mail": "loadtest@example.com",
            "subject[0][value]": "Performance testing inquiry",
            "message[0][value]": "This is a realistic Drupal load test submission generated by Locust.",
            "form_build_id": form_build_id.group(1),
            "form_token": form_token.group(1),
            "form_id": form_id.group(1),
            "op": "Send message"
        }
 
        self.client.post("/contact", data=payload, name="POST /contact")

Why test search and forms?

These scenarios often reveal hidden application costs:

  • Search index latency
  • Slow SQL queries
  • Form validation overhead
  • Spam protection delays
  • Mail delivery bottlenecks
  • Third-party integrations affecting response time

For Drupal sites with editorial workflows or lead-generation forms, these are high-value endpoints to include in stress testing.

Analyzing Your Results

After running your Drupal load test in LoadForge, focus on the metrics that tell you where the stack is breaking down.

Response time percentiles

Average response time is useful, but percentiles are more revealing:

  • P50 shows typical experience
  • P95 shows what slower users experience
  • P99 shows extreme tail latency

For Drupal, tail latency often grows first on:

  • Authenticated pages
  • Search pages
  • Commerce actions
  • API endpoints with filters or includes

Requests per second

Look at throughput by endpoint group:

  • Homepage
  • Content pages
  • Login
  • Search
  • Add to cart
  • Checkout
  • JSON:API

If throughput flattens while users increase, you may be hitting a resource ceiling in PHP, the database, or cache infrastructure.

Error rate

Common Drupal load testing errors include:

  • 500 Internal Server Error
  • 502/504 from reverse proxies
  • 403 due to CSRF or WAF rules
  • 429 if rate limiting is enabled
  • Failed redirects after login
  • Session-related failures in cart flows

LoadForge’s real-time reporting helps you identify when and where errors begin so you can correlate them with traffic ramps.

Endpoint grouping

Using the name parameter in Locust is especially important for Drupal because many pages are dynamic or path-specific. Grouping requests lets you compare classes of behavior:

  • Cached content pages versus uncached pages
  • Anonymous versus authenticated requests
  • Product pages versus checkout pages
  • Simple API calls versus filtered API calls

Infrastructure correlation

Pair LoadForge results with server-side metrics:

  • CPU and memory usage
  • PHP-FPM active/max children
  • Database connections and slow queries
  • Redis hit rates
  • CDN cache hit ratio
  • Nginx or Apache request queues

This is where distributed testing from multiple global test locations can be especially helpful. If one region performs worse, that may indicate CDN, DNS, or regional routing issues rather than Drupal itself.

Performance Optimization Tips

Based on common Drupal load testing results, here are practical optimization tips.

Improve caching strategy

  • Enable and tune Drupal page cache and Dynamic Page Cache
  • Use Varnish or a CDN for anonymous traffic
  • Reduce unnecessary cache invalidation
  • Cache API responses where possible

Tune PHP and web server capacity

  • Increase PHP-FPM workers if they are saturating
  • Review opcache settings
  • Optimize Nginx or Apache keepalive and worker settings
  • Ensure enough CPU for peak concurrency

Optimize database queries

  • Review slow query logs
  • Add indexes for frequently filtered fields
  • Reduce expensive Views configurations
  • Minimize entity loading in custom modules

Audit contributed and custom modules

  • Disable unused modules
  • Profile expensive hooks, subscribers, and preprocess functions
  • Avoid loading large related entities unnecessarily
  • Review Commerce customizations carefully

Optimize Drupal Commerce flows

  • Simplify promotions and tax calculations
  • Reduce checkout step complexity
  • Cache product data where possible
  • Offload external payment or shipping calls when appropriate

Improve API efficiency

  • Limit JSON:API payload size
  • Avoid overusing include
  • Paginate aggressively
  • Cache popular API responses
  • Reduce serialization overhead

Test continuously

Performance is not a one-time task. With LoadForge CI/CD integration, you can run Drupal performance testing regularly after deployments, infrastructure changes, or module updates.

Common Pitfalls to Avoid

Drupal load testing can produce misleading results if you make these common mistakes.

Testing only the homepage

A homepage test may mostly measure your CDN or reverse proxy. It will not reveal how Drupal behaves for logged-in users, search, forms, or commerce workflows.

Ignoring authenticated traffic

Many of the worst Drupal bottlenecks appear only when users are logged in. If your site has accounts, subscriptions, editorial roles, or commerce, you must test those flows.

Hardcoding form tokens blindly

Drupal forms often require dynamic hidden fields like form_build_id, form_token, and form_id. If you skip token extraction, your test may fail or generate unrealistic traffic.

Forgetting sessions and cookies

Cart flows, logins, and checkout depend on session continuity. Make sure each Locust user behaves like a real browser session.

Overlooking cache warm-up

Cold-cache and warm-cache performance are very different in Drupal. Run both kinds of tests so you understand real-world behavior after deployments or cache clears.

Not using realistic data

Testing with one article, one product, or one search query can hide scaling issues. Real Drupal sites have content variety, taxonomy depth, and uneven hot spots.

Running tests against production without safeguards

Stress testing production can disrupt real users. Prefer staging environments, or carefully control test windows, load levels, and IP allowlists.

Misreading success metrics

A site can have low error rates but still be too slow for users. Always evaluate latency, throughput, and user experience together.

Conclusion

Drupal is powerful, flexible, and capable of supporting high-traffic CMS and e-commerce workloads—but only if you validate its behavior under realistic load. Anonymous browsing, authenticated sessions, JSON:API traffic, search, forms, and Drupal Commerce checkout flows all place different demands on your stack. The only reliable way to uncover those limits is through structured load testing, performance testing, and stress testing.

With LoadForge, you can build realistic Locust-based Drupal test scripts, run distributed tests from global locations, monitor results in real time, and integrate performance checks into your CI/CD pipeline. That makes it much easier to catch regressions early and keep your Drupal site fast during heavy traffic.

If you’re ready to stress test your Drupal site and find bottlenecks before your users do, try LoadForge.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.