
Introduction
Symfony is a powerful PHP web framework used to build everything from content-heavy websites to complex enterprise applications and JSON APIs. It offers strong routing, dependency injection, event dispatching, caching, security, and database integration through Doctrine. But even well-architected Symfony applications can struggle under real-world traffic if critical bottlenecks go undetected.
That’s why load testing Symfony applications is essential before production releases. A proper load testing and performance testing strategy helps you understand how your application behaves under concurrent users, identify slow controllers, uncover Doctrine query inefficiencies, validate authentication flows, and test infrastructure components such as PHP-FPM, Nginx, Redis, MySQL, and message queues.
In this Symfony load testing guide with LoadForge, you’ll learn how to create realistic Locust scripts for Symfony applications, simulate authenticated user journeys, test API-heavy workloads, and analyze the results to improve production readiness. Because LoadForge is built on Locust, you can write flexible Python-based test scripts while taking advantage of cloud-based infrastructure, distributed testing, real-time reporting, CI/CD integration, and global test locations.
Prerequisites
Before you begin load testing your Symfony app, make sure you have the following:
- A running Symfony application in a staging or pre-production environment
- Test user accounts with appropriate roles such as
ROLE_USERandROLE_ADMIN - Access to realistic endpoints, such as:
/login/dashboard/products/cart/add/checkout/api/login_check/api/orders
- Sample test data in your database
- CSRF handling enabled and understood for form-based login flows
- Monitoring in place for your stack, such as:
- PHP-FPM metrics
- Nginx or Apache logs
- MySQL or PostgreSQL slow query logs
- Redis memory and latency
- Symfony profiler in staging if appropriate
- A LoadForge account for executing distributed load tests at scale
If your Symfony app uses JWT authentication, session-based login, API Platform, Doctrine ORM, or Symfony Messenger, it’s worth planning test scenarios around those components specifically.
Understanding Symfony Under Load
Symfony performance under concurrent traffic depends on far more than just controller logic. A Symfony request typically flows through several layers:
- Web server such as Nginx or Apache
- PHP runtime and PHP-FPM workers
- Symfony kernel bootstrapping
- Routing and middleware/event listeners
- Security firewall and authentication
- Controller execution
- Doctrine ORM/database operations
- Templating or JSON serialization
- Cache/session/message queue interactions
Under load, common Symfony bottlenecks include:
PHP-FPM Worker Saturation
If your app receives more concurrent requests than available PHP-FPM workers, requests queue up and response times increase rapidly. This often appears during login spikes, checkout flows, or expensive report generation.
Doctrine Query Inefficiencies
Doctrine is productive, but poorly optimized queries can become a major problem during stress testing. Common issues include:
- N+1 query patterns
- Missing indexes
- Over-fetching related entities
- Heavy hydration costs
- Long-running transactions
Session Locking
Many Symfony apps rely on session-based authentication. If session storage is file-based or poorly configured, concurrent requests from the same user can become serialized, reducing throughput and causing unpredictable latency.
Cache Misses and Warmup Problems
Symfony applications often perform much better with warmed caches. If your test starts against a cold environment, you may see unusually high initial response times. That’s useful in some cases, but you should distinguish cold-start behavior from steady-state performance.
Security and Authentication Overhead
Symfony’s security system is robust, but login flows, password hashing, token generation, and access control checks all add cost. This is especially important when testing APIs secured with JWT or session-based forms with CSRF validation.
Template Rendering and Serialization
Twig rendering for server-side pages and serializer overhead for APIs can become noticeable at scale, especially on endpoints returning large collections or deeply nested objects.
A good Symfony load testing plan should include both anonymous traffic and authenticated user flows, plus read-heavy and write-heavy scenarios.
Writing Your First Load Test
Let’s start with a basic Symfony load test that simulates anonymous visitors browsing common public pages. This is a good first step for validating routing, controller performance, Twig rendering, and cache behavior.
Basic Symfony Page Load Test
from locust import HttpUser, task, between
class SymfonyPublicUser(HttpUser):
wait_time = between(1, 3)
@task(4)
def homepage(self):
self.client.get("/", name="GET /")
@task(2)
def product_listing(self):
self.client.get("/products", name="GET /products")
@task(2)
def category_page(self):
self.client.get("/category/electronics", name="GET /category/:slug")
@task(1)
def product_detail(self):
self.client.get("/products/iphone-15-pro", name="GET /products/:slug")
@task(1)
def contact_page(self):
self.client.get("/contact", name="GET /contact")What This Test Covers
This script simulates general browsing behavior for a Symfony e-commerce or catalog-style app. It tests:
- Homepage rendering
- Product listing pages
- Category filtering
- Product detail pages
- Static informational pages
The name parameter groups dynamic URLs in LoadForge reports so you don’t end up with separate metrics for every slug.
Why This Matters for Symfony
Public pages often rely on:
- Twig templates
- HTTP caching headers
- Doctrine queries for lists and detail pages
- Redis or application caching
- Route matching and controller execution
If response times are already high here, authenticated flows will likely perform worse.
Advanced Load Testing Scenarios
Once basic browsing is covered, you should test realistic user journeys that reflect how Symfony applications are actually used in production.
Scenario 1: Session-Based Login with CSRF and Dashboard Access
Many Symfony apps use form-based login with CSRF protection. A realistic test should first fetch the login form, extract the CSRF token, submit credentials, and then access authenticated pages.
from locust import HttpUser, task, between
import re
class SymfonyAuthenticatedUser(HttpUser):
wait_time = between(2, 5)
def on_start(self):
self.login()
def login(self):
response = self.client.get("/login", name="GET /login")
match = re.search(r'name="_csrf_token"\s+value="([^"]+)"', response.text)
if not match:
response.failure("CSRF token not found on login page")
return
csrf_token = match.group(1)
login_data = {
"_username": "loadtest.user@example.com",
"_password": "P@ssw0rd123!",
"_csrf_token": csrf_token
}
with self.client.post(
"/login",
data=login_data,
allow_redirects=True,
name="POST /login",
catch_response=True
) as login_response:
if "Dashboard" not in login_response.text and "/dashboard" not in login_response.url:
login_response.failure("Login failed or dashboard not reached")
@task(3)
def dashboard(self):
self.client.get("/dashboard", name="GET /dashboard")
@task(2)
def account_profile(self):
self.client.get("/account/profile", name="GET /account/profile")
@task(1)
def order_history(self):
self.client.get("/account/orders", name="GET /account/orders")Why This Scenario Is Important
This test validates a realistic Symfony security workflow:
- Login form rendering
- CSRF token generation and validation
- Session creation
- Authenticated page access
- Role-based content rendering
This is especially useful for finding issues with:
- Slow password hashing configuration in staging
- Session storage performance
- Security firewall overhead
- Dashboard queries pulling too much data
If login throughput is poor, investigate session backend performance, password hashing cost, and any listeners or subscribers triggered on authentication.
Scenario 2: Symfony API with JWT Authentication
Many modern Symfony applications expose APIs using LexikJWTAuthenticationBundle or API Platform. In these apps, users authenticate once and then send a bearer token with subsequent requests.
from locust import HttpUser, task, between
import random
class SymfonyApiUser(HttpUser):
wait_time = between(1, 2)
token = None
def on_start(self):
self.authenticate()
def authenticate(self):
credentials = {
"username": "api.tester@example.com",
"password": "SecureApiPass123!"
}
with self.client.post(
"/api/login_check",
json=credentials,
name="POST /api/login_check",
catch_response=True
) as response:
if response.status_code == 200:
data = response.json()
self.token = data.get("token")
if not self.token:
response.failure("JWT token missing in response")
else:
response.failure(f"Authentication failed: {response.status_code}")
def auth_headers(self):
return {
"Authorization": f"Bearer {self.token}",
"Accept": "application/json"
}
@task(4)
def list_products(self):
params = {
"page": random.randint(1, 5),
"limit": 20,
"category": random.choice(["electronics", "books", "home"])
}
self.client.get(
"/api/products",
params=params,
headers=self.auth_headers(),
name="GET /api/products"
)
@task(2)
def product_detail(self):
product_id = random.randint(100, 250)
self.client.get(
f"/api/products/{product_id}",
headers=self.auth_headers(),
name="GET /api/products/:id"
)
@task(2)
def create_cart_item(self):
payload = {
"productId": random.randint(100, 250),
"quantity": random.randint(1, 3)
}
self.client.post(
"/api/cart/items",
json=payload,
headers=self.auth_headers(),
name="POST /api/cart/items"
)
@task(1)
def list_orders(self):
self.client.get(
"/api/orders",
headers=self.auth_headers(),
name="GET /api/orders"
)What This Tests in Symfony
This scenario is ideal for Symfony API performance testing because it exercises:
- JWT authentication
- API Platform or custom controller endpoints
- JSON serialization
- Doctrine pagination
- Cart and order business logic
This is where you may uncover:
- Slow normalizers or serializers
- Heavy joins in API collections
- Missing DB indexes on filters
- Excessive payload sizes
- Token validation overhead
When running this in LoadForge, you can scale users across multiple cloud regions to see how your Symfony API performs globally and whether latency-sensitive endpoints need CDN, caching, or regional infrastructure improvements.
Scenario 3: E-Commerce Checkout Flow with Cart and Order Creation
For many Symfony applications, the most important flow is not a single endpoint but a business transaction. A checkout journey often includes multiple reads and writes, inventory checks, pricing calculations, address validation, and payment initiation.
from locust import HttpUser, task, between
import random
import re
class SymfonyCheckoutUser(HttpUser):
wait_time = between(3, 6)
def on_start(self):
self.login()
def login(self):
response = self.client.get("/login", name="GET /login")
match = re.search(r'name="_csrf_token"\s+value="([^"]+)"', response.text)
if not match:
return
csrf_token = match.group(1)
self.client.post(
"/login",
data={
"_username": "shopper@example.com",
"_password": "CheckoutPass123!",
"_csrf_token": csrf_token
},
allow_redirects=True,
name="POST /login"
)
@task
def complete_checkout(self):
product_slug = random.choice([
"iphone-15-pro",
"sony-wh-1000xm5",
"kindle-paperwhite"
])
self.client.get(f"/products/{product_slug}", name="GET /products/:slug")
add_to_cart_data = {
"product_id": random.choice([101, 102, 103]),
"quantity": 1
}
self.client.post(
"/cart/add",
data=add_to_cart_data,
name="POST /cart/add"
)
self.client.get("/cart", name="GET /cart")
checkout_page = self.client.get("/checkout", name="GET /checkout")
match = re.search(r'name="_csrf_token"\s+value="([^"]+)"', checkout_page.text)
csrf_token = match.group(1) if match else ""
order_data = {
"checkout[billingAddress][firstName]": "Jane",
"checkout[billingAddress][lastName]": "Doe",
"checkout[billingAddress][street]": "123 Market Street",
"checkout[billingAddress][city]": "San Francisco",
"checkout[billingAddress][postalCode]": "94105",
"checkout[billingAddress][country]": "US",
"checkout[shippingMethod]": "standard",
"checkout[paymentMethod]": "credit_card",
"_csrf_token": csrf_token
}
with self.client.post(
"/checkout/complete",
data=order_data,
allow_redirects=True,
name="POST /checkout/complete",
catch_response=True
) as response:
if response.status_code not in [200, 302]:
response.failure(f"Checkout failed with status {response.status_code}")Why This Scenario Is Valuable
This kind of end-to-end stress testing is where Symfony bottlenecks often show up most clearly:
- Cart session updates
- Inventory checks
- Price calculations
- Form handling and validation
- Doctrine writes and transaction locks
- Email or message dispatch after order creation
If checkout slows down under load, investigate:
- Database write contention
- Transaction scope
- Synchronous event listeners
- Payment service call patterns
- Session locking
- Queue backlogs if using Symfony Messenger
This is also a great candidate for LoadForge’s distributed testing, since checkout behavior can vary under higher concurrency and across multiple traffic origins.
Analyzing Your Results
After running your Symfony load test in LoadForge, focus on more than just average response time. You want to understand how the framework and surrounding infrastructure behave as concurrency increases.
Key Metrics to Review
Response Time Percentiles
Look at p50, p95, and p99 response times for each endpoint:
GET /productsmay look fine at average latency, but p95 may reveal query spikesPOST /loginmay show occasional delays due to session or password verification overheadPOST /checkout/completeoften has the highest tail latency
Percentiles matter more than averages when evaluating real user experience.
Requests Per Second
This tells you how much traffic your Symfony app can sustain. If RPS plateaus while response times climb, you may be hitting limits in:
- PHP-FPM workers
- Database connections
- CPU
- Session backend
- Reverse proxy capacity
Error Rate
Watch for:
500errors from unhandled exceptions502or504from upstream timeouts429if rate limiting is enabled403or401from auth misconfiguration during testing- CSRF validation failures on form workflows
Endpoint Grouping
Use Locust name values consistently so LoadForge reports show grouped metrics like:
GET /products/:slugGET /api/products/:idPOST /checkout/complete
This makes it much easier to identify route-level bottlenecks.
Correlate LoadForge Results with Backend Monitoring
For Symfony applications, pair LoadForge metrics with:
- PHP-FPM process utilization
- CPU and memory usage
- Database query times and lock waits
- Redis latency
- Session store performance
- Web server connection counts
If response times spike while CPU remains low, the issue may be I/O, database locking, or worker starvation rather than raw compute limits.
Compare Warm and Cold Performance
Run tests both before and after cache warmup. Symfony can behave very differently depending on whether caches are primed. This is especially relevant after deployments.
Use Step Load and Stress Testing
Don’t just test at one traffic level. Increase users gradually to find the breaking point. For example:
- 25 users for baseline
- 100 users for expected production traffic
- 300+ users for stress testing
LoadForge makes this easy with cloud-based scaling and real-time reporting, so you can see exactly when Symfony performance starts to degrade.
Performance Optimization Tips
Here are practical ways to improve Symfony performance after load testing reveals bottlenecks.
Optimize Doctrine Queries
- Add indexes for frequently filtered columns
- Avoid N+1 queries with joins or eager loading where appropriate
- Paginate large result sets
- Select only needed fields for API responses
- Review hydration costs for complex entities
Tune PHP-FPM
- Increase worker counts based on available CPU and memory
- Monitor max children exhaustion
- Adjust request termination and slow log settings
- Make sure opcache is properly configured
Improve Caching
- Use HTTP caching for public content where possible
- Cache expensive computations in Redis
- Warm Symfony cache after deployments
- Use fragment caching for repeated page components
Reduce Session Contention
- Move from file-based sessions to Redis or another scalable backend
- Minimize unnecessary session writes
- Avoid locking-heavy workflows where possible
Optimize Authentication
- Review password hashing cost in non-production environments used for testing
- Cache user-related lookups when appropriate
- Keep JWT payloads lean
- Avoid expensive listeners during login if not essential
Offload Heavy Work
If checkout or registration triggers synchronous email sending, PDF generation, or analytics calls, move those tasks to Symfony Messenger so the user-facing request stays fast.
Profile Serialization and Templates
- Reduce unnecessary fields in API responses
- Avoid deeply nested serializer groups unless needed
- Review Twig templates for expensive loops or repeated DB access
Common Pitfalls to Avoid
Symfony load testing is highly effective, but there are several common mistakes that can distort results.
Ignoring CSRF in Form-Based Flows
If your app uses Symfony forms and CSRF protection, posting directly to /login or /checkout/complete without first fetching a valid token will produce unrealistic failures.
Testing with Unrealistic User Behavior
Don’t make every virtual user hit only the homepage or only the login endpoint. Real traffic is mixed. Build scenarios that reflect actual Symfony usage patterns.
Using Empty or Unrealistic Databases
A Symfony app with 20 products behaves very differently from one with 200,000 products and years of order history. Load test against realistic data volumes.
Forgetting Background Dependencies
Your Symfony app may depend on:
- Redis
- MySQL/PostgreSQL
- Elasticsearch
- Messenger workers
- External payment or email services
If those are not represented in staging, your performance testing results may be misleading.
Not Grouping Dynamic Routes
If every product slug or order ID appears as a separate metric, reports become noisy and hard to analyze. Use consistent Locust naming.
Overlooking Ramp-Up Effects
A sudden spike to hundreds of users is useful for stress testing, but it’s different from gradual growth. Use both patterns to understand Symfony behavior under different traffic conditions.
Testing Production Without Safeguards
Never run aggressive stress testing against production without clear controls, traffic limits, and stakeholder approval. Checkout, login, and write-heavy endpoints can create real business impact.
Conclusion
Load testing Symfony applications is one of the best ways to validate production readiness, improve response times, and identify bottlenecks before users do. Whether you’re testing public pages, form-based authentication, JWT-secured APIs, or complex checkout flows, realistic Locust scripts give you the flexibility to simulate true user behavior and uncover meaningful performance issues.
With LoadForge, you can run Symfony load testing at scale using distributed cloud-based infrastructure, monitor results in real time, integrate tests into CI/CD pipelines, and launch traffic from global test locations. If you’re serious about Symfony performance testing and stress testing, now is the perfect time to build your first scenario and see how your application performs under real load.
Try LoadForge and start load testing your Symfony app today.
LoadForge Team
LoadForge is a load and performance testing platform built on Locust. Our team has been shipping load tests against production systems since 2018, and we write these guides from real customer engagements.
Related guides
Keep going with more guides from the same category.

ASP.NET Load Testing Guide with LoadForge
Learn how to load test ASP.NET applications with LoadForge to find performance issues and ensure your app handles peak traffic.

CakePHP Load Testing Guide with LoadForge
Load test CakePHP applications with LoadForge to benchmark app performance, simulate traffic, and improve scalability.

Django Load Testing Guide with LoadForge
Discover how to load test Django applications with LoadForge to measure performance, handle traffic spikes, and improve stability.