LoadForge LogoLoadForge

Load Testing SOAP APIs with LoadForge

Load Testing SOAP APIs with LoadForge

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/xml or application/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.

python
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/xml for 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.

python
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:

  1. Call Login
  2. Store a session token
  3. Call SearchOrders
  4. Call GetOrderDetails
python
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
python
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 Login
  • SOAP SearchOrders
  • SOAP GetOrderDetails
  • SOAP 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.

Try LoadForge free for 7 days

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