
Introduction
SOAP APIs still power many mission-critical systems in finance, healthcare, insurance, telecom, and enterprise integration. While REST and GraphQL often get more attention, SOAP remains a common choice for operations that require strict contracts, WS-Security, XML schemas, and reliable messaging. If your application depends on SOAP services, load testing is essential to validate XML parsing performance, request throughput, response times, and service stability under concurrent traffic.
Load testing SOAP APIs is different from testing lightweight JSON endpoints. SOAP requests often include larger XML envelopes, strict namespaces, authentication headers, and schema validation. These characteristics can increase CPU usage, memory pressure, and latency on both clients and servers. A SOAP service that performs well with a handful of users may degrade quickly when exposed to hundreds or thousands of concurrent requests.
In this guide, you’ll learn how to load test SOAP APIs with LoadForge using Locust-based Python scripts. We’ll cover realistic SOAP request patterns, authentication approaches, XML payload construction, and advanced scenarios like session management and multi-operation workflows. You’ll also see how LoadForge’s distributed testing, real-time reporting, cloud-based infrastructure, CI/CD integration, and global test locations can help you run meaningful performance testing and stress testing for SOAP services at scale.
Prerequisites
Before you start load testing SOAP APIs with LoadForge, make sure you have:
- A SOAP API endpoint to test, such as:
/services/CustomerService/soap/OrderProcessing/ws/BillingService
- The WSDL or service documentation for your SOAP API
- Sample SOAP request and response XML for key operations
- Authentication details, if required:
- Basic Authentication
- Bearer token
- WS-Security UsernameToken
- Session-based authentication
- Test data such as customer IDs, account numbers, order IDs, or invoice references
- Permission to run load testing against the target environment
- A LoadForge account for running distributed load testing in the cloud
It also helps to understand:
- SOAPAction headers
- XML namespaces
- Content-Type requirements, usually
text/xmlorapplication/soap+xml - The difference between SOAP 1.1 and SOAP 1.2
- Any server-side rate limits, message size limits, or gateway protections
Understanding SOAP APIs Under Load
SOAP APIs behave differently under load than many modern HTTP APIs because XML processing is typically more expensive than JSON parsing. Every request may involve:
- XML envelope parsing
- Namespace resolution
- XSD schema validation
- Security token validation
- Transformation or routing through middleware like ESBs or API gateways
- Backend calls to databases, legacy systems, or message queues
Under concurrent load, common bottlenecks include:
XML Parsing Overhead
Large SOAP envelopes increase CPU consumption. If your service handles deeply nested XML or attachments, parsing can become a major source of latency.
Authentication and Security Processing
SOAP services often use WS-Security, mutual TLS, or enterprise SSO layers. Security validation can add noticeable overhead, especially when every request includes signed headers or UsernameToken processing.
Backend Integration Delays
SOAP APIs frequently act as wrappers around ERP, CRM, billing, or mainframe systems. The SOAP layer itself may be fast, while the downstream dependency becomes the real bottleneck during performance testing.
Thread Pool and Connection Limits
SOAP services hosted on Java app servers, .NET services, or integration platforms may have strict limits on request worker threads, database pools, or connection handlers. Load testing helps reveal where concurrency starts to overwhelm these resources.
Payload Size and Serialization Costs
Operations like invoice retrieval, policy lookup, order submission, or customer synchronization may involve large XML responses. This affects response time, bandwidth, and memory usage under load.
When you run load testing or stress testing against SOAP APIs, you want to measure:
- Requests per second and throughput
- Median and percentile response times
- Error rates and timeout frequency
- Behavior under sustained concurrency
- Performance differences between lightweight and heavy SOAP operations
Writing Your First Load Test
Let’s start with a simple SOAP load test that calls a GetCustomerDetails operation. This example uses SOAP 1.1 with a SOAPAction header and HTTP Basic Authentication.
Assume the endpoint is:
https://api.example-insurance.com/services/CustomerService
And the operation takes a customer ID and returns profile details.
from locust import HttpUser, task, between
import random
class SoapCustomerUser(HttpUser):
wait_time = between(1, 3)
customer_ids = [
"CUST10001",
"CUST10002",
"CUST10003",
"CUST10004",
"CUST10005"
]
@task
def get_customer_details(self):
customer_id = random.choice(self.customer_ids)
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://example.com/customer/GetCustomerDetails"
}
payload = f"""<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:cust="http://example.com/customer">
<soapenv:Header/>
<soapenv:Body>
<cust:GetCustomerDetailsRequest>
<cust:CustomerId>{customer_id}</cust:CustomerId>
</cust:GetCustomerDetailsRequest>
</soapenv:Body>
</soapenv:Envelope>"""
with self.client.post(
"/services/CustomerService",
data=payload,
headers=headers,
auth=("loadtest_user", "SuperSecurePassword123"),
name="SOAP GetCustomerDetails",
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"Unexpected status code: {response.status_code}")
elif "<cust:GetCustomerDetailsResponse" not in response.text:
response.failure("SOAP response missing expected element")
elif "<cust:Status>SUCCESS</cust:Status>" not in response.text:
response.failure("SOAP operation did not return SUCCESS")
else:
response.success()This script demonstrates several SOAP-specific load testing practices:
- Uses
text/xmlfor SOAP 1.1 - Sets a realistic
SOAPAction - Sends a full XML envelope
- Includes HTTP Basic Authentication
- Validates both HTTP status and SOAP response content
This is a good starting point for performance testing a single read-heavy SOAP operation. In LoadForge, you can scale this script across multiple cloud workers to simulate traffic from distributed users and observe real-time response trends.
Advanced Load Testing Scenarios
Once the basic request works, you should test more realistic SOAP API workflows. Production SOAP traffic usually includes authentication, multiple operations, and a mix of lightweight and heavyweight requests.
Scenario 1: SOAP API with WS-Security UsernameToken
Many enterprise SOAP APIs use WS-Security headers rather than HTTP auth. Here’s a realistic login-free request that embeds a UsernameToken in the SOAP header.
Assume the endpoint is:
https://payments.examplecorp.com/ws/BillingService
And the operation is GetInvoiceStatus.
from locust import HttpUser, task, between
import random
from datetime import datetime, timezone
import uuid
class SoapBillingUser(HttpUser):
wait_time = between(2, 5)
invoice_ids = [
"INV-2024-10001",
"INV-2024-10002",
"INV-2024-10003",
"INV-2024-10004"
]
def build_ws_security_header(self, username, password):
created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
nonce = str(uuid.uuid4())
return f"""
<wsse:Security soapenv:mustUnderstand="1"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:UsernameToken wsu:Id="UsernameToken-{nonce}">
<wsse:Username>{username}</wsse:Username>
<wsse:Password>{password}</wsse:Password>
<wsse:Nonce>{nonce}</wsse:Nonce>
<wsu:Created>{created}</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
"""
@task
def get_invoice_status(self):
invoice_id = random.choice(self.invoice_ids)
security_header = self.build_ws_security_header("billing_api_user", "BillingPassword!")
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://example.com/billing/GetInvoiceStatus"
}
payload = f"""<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:bill="http://example.com/billing">
<soapenv:Header>
{security_header}
</soapenv:Header>
<soapenv:Body>
<bill:GetInvoiceStatusRequest>
<bill:InvoiceId>{invoice_id}</bill:InvoiceId>
</bill:GetInvoiceStatusRequest>
</soapenv:Body>
</soapenv:Envelope>"""
with self.client.post(
"/ws/BillingService",
data=payload,
headers=headers,
name="SOAP GetInvoiceStatus WS-Security",
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"HTTP {response.status_code}")
elif "<bill:GetInvoiceStatusResponse" not in response.text:
response.failure("Missing invoice status response")
elif "<bill:InvoiceState>" not in response.text:
response.failure("InvoiceState not found in SOAP response")
else:
response.success()This example is useful for stress testing SOAP services where security header parsing is part of the normal transaction cost. It more accurately reflects enterprise SOAP workloads than simple HTTP auth.
Scenario 2: Session-Based Authentication and Multi-Step Workflow
Some SOAP APIs require an explicit login operation that returns a session token, followed by business operations using that token in the SOAP header. This is common in older CRM, ERP, and order management systems.
In this example, users:
- Call
Login - Store a session token
- Call
SearchOrders - Call
GetOrderDetails
from locust import HttpUser, task, between
import random
import re
class SoapOrderUser(HttpUser):
wait_time = between(1, 4)
order_ids = ["ORD-900101", "ORD-900102", "ORD-900103", "ORD-900104"]
session_token = None
def on_start(self):
self.login()
def extract_session_token(self, response_text):
match = re.search(r"<ord:SessionToken>(.*?)</ord:SessionToken>", response_text)
return match.group(1) if match else None
def login(self):
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://example.com/order/Login"
}
payload = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://example.com/order">
<soapenv:Header/>
<soapenv:Body>
<ord:LoginRequest>
<ord:Username>loadtest_orders</ord:Username>
<ord:Password>OrderTestPassword!</ord:Password>
</ord:LoginRequest>
</soapenv:Body>
</soapenv:Envelope>"""
response = self.client.post(
"/soap/OrderProcessing",
data=payload,
headers=headers,
name="SOAP Login"
)
self.session_token = self.extract_session_token(response.text)
def build_session_header(self):
return f"""
<ord:SessionHeader xmlns:ord="http://example.com/order">
<ord:SessionToken>{self.session_token}</ord:SessionToken>
</ord:SessionHeader>
"""
@task(2)
def search_orders(self):
if not self.session_token:
self.login()
customer_id = random.choice(["CUST10001", "CUST10002", "CUST10003"])
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://example.com/order/SearchOrders"
}
payload = f"""<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://example.com/order">
<soapenv:Header>
{self.build_session_header()}
</soapenv:Header>
<soapenv:Body>
<ord:SearchOrdersRequest>
<ord:CustomerId>{customer_id}</ord:CustomerId>
<ord:Status>SHIPPED</ord:Status>
<ord:CreatedAfter>2024-01-01</ord:CreatedAfter>
</ord:SearchOrdersRequest>
</soapenv:Body>
</soapenv:Envelope>"""
with self.client.post(
"/soap/OrderProcessing",
data=payload,
headers=headers,
name="SOAP SearchOrders",
catch_response=True
) as response:
if response.status_code != 200 or "<ord:SearchOrdersResponse" not in response.text:
response.failure("SearchOrders failed")
else:
response.success()
@task(1)
def get_order_details(self):
if not self.session_token:
self.login()
order_id = random.choice(self.order_ids)
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://example.com/order/GetOrderDetails"
}
payload = f"""<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://example.com/order">
<soapenv:Header>
{self.build_session_header()}
</soapenv:Header>
<soapenv:Body>
<ord:GetOrderDetailsRequest>
<ord:OrderId>{order_id}</ord:OrderId>
</ord:GetOrderDetailsRequest>
</soapenv:Body>
</soapenv:Envelope>"""
with self.client.post(
"/soap/OrderProcessing",
data=payload,
headers=headers,
name="SOAP GetOrderDetails",
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"HTTP {response.status_code}")
elif "<ord:GetOrderDetailsResponse" not in response.text:
response.failure("Missing order details response")
elif "<ord:OrderTotal>" not in response.text:
response.failure("OrderTotal not found in response")
else:
response.success()This kind of workflow is especially valuable for load testing because it reflects actual user behavior more closely than isolated requests. It also exposes session store bottlenecks, authentication server overhead, and stateful service limitations.
Scenario 3: Large XML Payload Submission for Create Operations
SOAP APIs often accept large structured messages for business transactions like claim submission, order creation, or policy updates. These write-heavy operations are critical to test because they usually consume more CPU, validation time, and database resources.
Assume a healthcare claims endpoint:
https://claims.examplehealth.com/services/ClaimSubmissionService
Operation:
SubmitClaim
from locust import HttpUser, task, between
import random
import uuid
from datetime import date
class SoapClaimSubmissionUser(HttpUser):
wait_time = between(3, 6)
procedure_codes = ["99213", "80050", "93000", "36415"]
diagnosis_codes = ["J10.1", "E11.9", "I10", "M54.5"]
@task
def submit_claim(self):
claim_id = str(uuid.uuid4())
member_id = f"MBR{random.randint(100000, 999999)}"
provider_id = f"PRV{random.randint(10000, 99999)}"
procedure_code = random.choice(self.procedure_codes)
diagnosis_code = random.choice(self.diagnosis_codes)
service_date = date.today().isoformat()
charge_amount = round(random.uniform(75.00, 450.00), 2)
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://example.com/claims/SubmitClaim"
}
payload = f"""<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:clm="http://example.com/claims">
<soapenv:Header/>
<soapenv:Body>
<clm:SubmitClaimRequest>
<clm:ClaimHeader>
<clm:ClaimId>{claim_id}</clm:ClaimId>
<clm:MemberId>{member_id}</clm:MemberId>
<clm:ProviderId>{provider_id}</clm:ProviderId>
<clm:ServiceDate>{service_date}</clm:ServiceDate>
</clm:ClaimHeader>
<clm:ClaimLines>
<clm:ClaimLine>
<clm:LineNumber>1</clm:LineNumber>
<clm:ProcedureCode>{procedure_code}</clm:ProcedureCode>
<clm:DiagnosisCode>{diagnosis_code}</clm:DiagnosisCode>
<clm:ChargeAmount>{charge_amount}</clm:ChargeAmount>
<clm:Units>1</clm:Units>
</clm:ClaimLine>
</clm:ClaimLines>
</clm:SubmitClaimRequest>
</soapenv:Body>
</soapenv:Envelope>"""
with self.client.post(
"/services/ClaimSubmissionService",
data=payload,
headers=headers,
name="SOAP SubmitClaim",
catch_response=True,
timeout=30
) as response:
if response.status_code != 200:
response.failure(f"Unexpected HTTP status: {response.status_code}")
elif "<clm:SubmitClaimResponse" not in response.text:
response.failure("SubmitClaim response missing")
elif "<clm:Accepted>true</clm:Accepted>" not in response.text:
response.failure("Claim was not accepted")
else:
response.success()This script is useful for performance testing payload-heavy SOAP operations where schema validation and persistence are likely to be expensive. It also helps identify whether throughput drops significantly for write operations compared to read-only requests.
Analyzing Your Results
After you run your SOAP API load testing in LoadForge, focus on more than just average response time. SOAP services often show performance problems first in tail latency and error patterns.
Key metrics to review include:
Response Time Percentiles
Look at p50, p95, and p99 response times. SOAP APIs may appear stable at the median while a subset of requests becomes very slow under concurrency due to XML parsing, thread contention, or backend delays.
Throughput
Measure requests per second for each SOAP operation. Compare lightweight operations like lookups against heavyweight operations like submissions or updates. A large drop in throughput for write operations often points to validation or database bottlenecks.
Error Rates
Watch for:
- HTTP 500 errors
- HTTP 502/503 gateway failures
- Timeouts
- SOAP Fault responses
- Authentication failures
- Schema validation errors
If possible, separate transport-level errors from business-level SOAP faults.
Per-Endpoint and Per-Operation Performance
Use Locust request names like:
SOAP LoginSOAP SearchOrdersSOAP GetOrderDetailsSOAP SubmitClaim
This makes LoadForge’s real-time reporting easier to interpret. You can quickly identify which SOAP operation is degrading under load.
Ramp-Up Behavior
A SOAP service may handle 50 users easily but fail at 200 users. Review how latency and errors change during ramp-up. LoadForge’s cloud-based infrastructure makes it easy to gradually scale concurrency and observe where saturation begins.
Geographic Performance
If your SOAP API serves users across regions, use LoadForge’s global test locations to identify latency differences caused by network distance, regional gateways, or geographically distributed infrastructure.
Performance Optimization Tips
Once your load testing reveals issues, these are common SOAP API optimization opportunities:
Reduce XML Payload Size
Remove unnecessary fields from requests and responses where possible. Large envelopes increase parsing time and bandwidth usage.
Optimize XML Parsing and Serialization
Use efficient XML libraries and avoid repeated namespace or schema processing where caching is possible. In Java and .NET environments, parser configuration can make a significant difference.
Cache Reference Data
If SOAP operations repeatedly fetch static lookup data, caching can reduce backend load and improve response times.
Tune Thread Pools and Connection Pools
Application servers, SOAP runtimes, and database pools should be sized for your expected concurrency. Load testing helps determine the right thresholds.
Review Authentication Overhead
If WS-Security or session validation is expensive, profile those code paths. Authentication can become a bottleneck before business logic does.
Optimize Backend Queries
SOAP services often hide slow SQL queries or mainframe calls behind a clean XML interface. If one operation is slow under load, the real issue may be downstream.
Test Realistic Mixes of Operations
Don’t load test only one endpoint. A realistic transaction mix gives more accurate performance testing results and helps avoid false confidence.
Common Pitfalls to Avoid
SOAP API load testing can go wrong if the test script doesn’t reflect real production behavior. Avoid these common mistakes:
Using Invalid or Repeated Test Data
If every request uses the same customer ID, order ID, or claim number, you may get unrealistic cache hits or duplicate processing issues. Vary your test data.
Ignoring SOAP Faults
A service may return HTTP 200 with a SOAP Fault inside the response body. Always validate the XML response, not just the status code.
Skipping Authentication Realism
If production uses WS-Security or session tokens, don’t replace it with a simplified unauthenticated test unless you are intentionally isolating one layer.
Testing Only Small Payloads
SOAP APIs often struggle with large and complex XML documents. Include realistic payload sizes in your stress testing.
Forgetting Headers Like SOAPAction
Many SOAP 1.1 services depend on correct SOAPAction values. Missing or incorrect headers can produce misleading failures.
Not Separating Operations by Name
If every request is labeled the same in Locust, your LoadForge reports become harder to analyze. Name requests clearly by SOAP operation.
Overlooking Environment Constraints
Test environments sometimes have lower capacity than production, shared databases, or debugging enabled. Interpret results in the context of the environment you tested.
Conclusion
Load testing SOAP APIs is essential for understanding how well your services handle XML-heavy traffic, authentication overhead, concurrent sessions, and backend integration pressure. With realistic Locust scripts in LoadForge, you can evaluate throughput, response times, and failure behavior for the SOAP operations that matter most to your business.
Whether you’re testing customer lookups, billing queries, order workflows, or large claim submissions, LoadForge gives you the tools to run distributed load testing at scale, analyze results in real time, and integrate performance testing into your CI/CD pipeline. If you’re ready to validate the resilience of your SOAP services, try LoadForge and start building SOAP load tests that reflect real-world production traffic.
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.

How to Load Test API Rate Limiting with LoadForge
Test API rate limiting with LoadForge to verify throttling rules, retry behavior, and service stability during traffic spikes.

Load Testing API Gateways with LoadForge
Discover how to load test API gateways with LoadForge to measure routing performance, latency, and resilience under heavy traffic.

Load Testing GraphQL APIs with LoadForge
Discover how to load test GraphQL APIs with LoadForge, including queries, mutations, concurrency, and performance bottlenecks.