AJAX - AJAX Resilience Through Bulkhead Isolation

 

1. Introduction

AJAX (Asynchronous JavaScript and XML) is a web development technique that allows a web page to communicate with a server without reloading the entire page. It enables applications to retrieve data, submit forms, update content, and perform other operations in the background.

Modern web applications often send multiple AJAX requests simultaneously. For example, an e-commerce website may send separate requests to retrieve product details, customer reviews, shopping cart information, payment status, and delivery estimates.

If one group of requests experiences delays or failures, it can sometimes consume shared resources and affect other parts of the application. This is where bulkhead isolation becomes useful.

Bulkhead isolation is a resilience technique that separates resources and operations into independent groups so that a failure in one group does not affect the entire application. When applied to AJAX-based applications, it helps prevent slow or failing requests in one feature from interfering with requests made by other features.

The term bulkhead comes from ship design. Ships use internal partitions to prevent water from flooding one compartment from spreading throughout the entire vessel. Similarly, software bulkheads separate resources to limit the impact of failures.

2. What Is Bulkhead Isolation in AJAX?

Bulkhead isolation in AJAX involves dividing asynchronous operations into separate groups and assigning limits to the resources each group can consume.

For example, consider a shopping website with the following features:

  • Product information retrieval

  • Customer review loading

  • Shopping cart updates

  • Payment processing

  • Recommendation loading

Suppose the recommendation service becomes slow and starts receiving a large number of requests. If every feature shares the same request queue and concurrency limit, recommendation requests may occupy all available request slots. Consequently, the shopping cart or product information may also become delayed.

Bulkhead isolation prevents this situation by allocating independent request limits to different features.

For example, the application might permit:

  • Four concurrent requests for product information.

  • Two concurrent requests for customer reviews.

  • Two concurrent requests for recommendations.

  • Two concurrent requests for shopping cart operations.

If recommendation requests become slow, they cannot consume all the slots reserved for the other groups.

These numbers are illustrative. Actual limits depend on the application's workload, server capacity, network conditions, and user experience requirements.

3. Why Is Bulkhead Isolation Necessary?

AJAX applications depend on asynchronous communication, but asynchronous execution alone does not prevent resource exhaustion or cascading delays.

Bulkhead isolation addresses several common problems.

A. Slow API responses

Some server endpoints may take longer to respond because of database queries, external service calls, or heavy processing. If too many slow requests accumulate, they may occupy available resources for an extended period.

Separating requests into groups prevents one slow endpoint from monopolizing every request slot.

B. Request queue congestion

When an application allows unlimited requests, a user action such as repeatedly clicking a button can generate a large number of requests.

These requests may compete for browser connections, server capacity, memory, and processing time.

Bulkhead isolation establishes limits on the number of active requests within each group and prevents one feature from overwhelming the entire application.

C. Cascading failures

A cascading failure occurs when a problem in one component causes other components to fail or become unavailable.

For example, a slow product recommendation service may delay a page if the application waits for all its requests to finish before displaying the main content.

Bulkhead isolation limits the impact of the failing recommendation feature, allowing essential features to continue working.

D. Unequal importance of requests

Not every AJAX request has the same business importance. Retrieving a product description may be helpful, but submitting an order or updating a shopping cart may be essential.

Bulkhead isolation allows developers to reserve resources for important operations instead of letting optional requests consume all available capacity.

However, resource separation alone does not guarantee that critical requests always succeed. Appropriate timeouts, prioritization, and server-side capacity planning are also necessary.

4. How Bulkhead Isolation Works

Bulkhead isolation can be implemented using separate request queues, concurrency limits, dedicated workers, or independent resource pools.

The general process involves the following steps.

Step 1: Identify request categories

First, developers examine the application's AJAX operations and group them according to their purpose.

For example:

  • Critical operations: placing orders and updating account details.

  • Standard operations: retrieving product information.

  • Optional operations: loading recommendations and additional reviews.

Grouping requests helps developers determine which operations should share resources and which should remain isolated.

Step 2: Assign resource limits

Each group receives a maximum number of concurrent requests.

For example, the recommendation group might allow only two active requests at a time. If two requests are already running, additional recommendation requests must wait in a queue or be rejected according to the application's policy.

The limit prevents an optional feature from creating an excessive number of simultaneous operations.

Step 3: Monitor active requests

The application tracks the number of requests currently running in each group.

When a request completes, fails, or times out, its slot is released. Another waiting request can then begin.

This process ensures that request limits are enforced consistently.

Step 4: Handle overload conditions

When a group reaches its concurrency limit, the application must decide what to do with additional requests.

Possible strategies include:

  • Queueing requests until a slot becomes available.

  • Rejecting requests that exceed the limit.

  • Discarding outdated requests when newer requests replace them.

  • Displaying a temporary message when an optional feature is unavailable.

The appropriate strategy depends on whether the operation is essential, optional, repeatable, or safe to cancel.

Step 5: Keep other groups operational

If the recommendation group becomes overloaded, the product and shopping cart groups should continue operating within their own resource limits.

This is the main purpose of bulkhead isolation: to contain a failure rather than allowing it to spread across the application.

5. Practical Example Using JavaScript

Consider a web page that retrieves product information and recommendations from different API endpoints. The recommendation service is slower than expected, so the application should limit the number of simultaneous recommendation requests.

The following example demonstrates a simple concurrency limiter that can be used to create independent request pools.

function createRequestPool(limit) {
    let activeRequests = 0;
    const queue = [];

    function runNext() {
        while (activeRequests < limit && queue.length > 0) {
            const task = queue.shift();
            activeRequests++;

            Promise.resolve()
                .then(task)
                .then(task.resolve, task.reject)
                .finally(() => {
                    activeRequests--;
                    runNext();
                });
        }
    }

    return function schedule(task) {
        return new Promise((resolve, reject) => {
            queue.push({
                ...{ resolve, reject },
                ...{ task }
            });

            runNext();
        });
    };
}

The pool above needs one correction to execute queued tasks: the queued function must be invoked rather than passed directly to Promise.resolve(). Here is the complete, corrected implementation.

function createRequestPool(limit) {
    let activeRequests = 0;
    const queue = [];

    function runNext() {
        while (activeRequests < limit && queue.length > 0) {
            const item = queue.shift();
            activeRequests++;

            Promise.resolve()
                .then(item.task)
                .then(item.resolve, item.reject)
                .finally(() => {
                    activeRequests--;
                    runNext();
                });
        }
    }

    return function schedule(task) {
        return new Promise((resolve, reject) => {
            queue.push({ task, resolve, reject });
            runNext();
        });
    };
}

const productRequests = createRequestPool(4);
const recommendationRequests = createRequestPool(2);

function fetchProduct(id) {
    return productRequests(() =>
        fetch(`/api/products/${id}`)
    );
}

function fetchRecommendations(id) {
    return recommendationRequests(() =>
        fetch(`/api/products/${id}/recommendations`)
    );
}

fetchProduct(101)
    .then(response => {
        if (!response.ok) {
            throw new Error("Product request failed");
        }
        return response.json();
    })
    .then(data => {
        console.log("Product information:", data);
    })
    .catch(error => {
        console.error("Product error:", error);
    });

fetchRecommendations(101)
    .then(response => {
        if (!response.ok) {
            throw new Error("Recommendation request failed");
        }
        return response.json();
    })
    .then(data => {
        console.log("Recommendations:", data);
    })
    .catch(error => {
        console.error("Recommendation error:", error);
    });

Explanation of the code

createRequestPool(limit)

This function creates a request pool with a specified maximum number of concurrent operations. Each pool maintains its own active-request count and queue.

activeRequests

This variable tracks how many tasks are currently executing in the pool. It prevents the number of active tasks from exceeding the configured limit.

queue

The queue stores tasks that are waiting for an available slot. When a running task completes, the next waiting task can begin.

runNext()

This function starts queued tasks while the number of active requests remains below the limit. It also releases the occupied slot after a task completes, whether successfully or unsuccessfully.

productRequests and recommendationRequests

These are two independent request pools. The product pool allows up to four active operations, while the recommendation pool allows up to two.

If the recommendation pool becomes busy, additional recommendation tasks wait in its queue. They do not occupy the product pool's concurrency slots.

Error handling

The .catch() handlers handle errors from the individual requests. Checking response.ok is important because fetch() does not automatically reject its promise when the server returns an HTTP error status such as 404 or 500.

This example demonstrates client-side concurrency isolation. In a production application, developers should also add queue-size limits, request timeouts, cancellation where appropriate, and server-side protection. The limiter shown here uses an unlimited waiting queue, so a large burst of requests could still consume memory.

6. Bulkhead Isolation Compared with Other Resilience Techniques

Bulkhead isolation is often used together with other techniques, but each solves a different problem.

Technique

Main purpose

Bulkhead isolation

Limits the resources one request group can consume.

Timeout

Stops waiting indefinitely for a slow operation.

Retry

Attempts an operation again after a recoverable failure.

Circuit breaker

Temporarily stops calls to a service that is repeatedly failing.

Request cancellation

Stops work that is no longer needed, when cancellation is supported.

Rate limiting

Controls how frequently requests can be initiated over a period of time.

For example, a recommendation request might have a concurrency limit of two, a timeout of three seconds, and a circuit breaker that temporarily blocks requests after repeated failures.

These mechanisms complement one another. Bulkhead isolation controls resource allocation, while timeouts limit waiting time and circuit breakers help prevent repeated calls to an unhealthy service.

Retries require special care because they can increase traffic during an outage. Operations that change data, such as placing an order, should not be retried blindly without suitable idempotency protections.

7. Advantages of Bulkhead Isolation in AJAX

Improved application stability

Separating request groups prevents an overloaded feature from consuming all the resources available to the other features.

Better user experience

Essential content and interactions can remain responsive even when optional features experience delays.

Controlled resource consumption

Concurrency limits help control the number of active operations and reduce unnecessary pressure on browser and server resources.

Easier troubleshooting

When request groups are separated and monitored independently, developers can identify which feature is experiencing excessive delays or failures.

Greater fault tolerance

The application can continue offering its core functionality even when one supporting service becomes temporarily unavailable.

8. Limitations and Challenges

Although bulkhead isolation is useful, it does not solve every performance or reliability problem.

Poorly chosen limits: Limits that are too low can delay legitimate requests. Limits that are too high may fail to protect the application from overload.

Queue growth: A queue without a maximum size can grow indefinitely when incoming requests arrive faster than they finish.

Shared bottlenecks: Separate client-side queues do not isolate a shared backend, database, browser main thread, or network connection. Those resources may still become overloaded.

Complexity: Multiple request pools require careful configuration, monitoring, and maintenance.

Essential operations: Rejecting or canceling an important operation can result in an incomplete user action. Applications must distinguish safe-to-drop operations from operations that need guaranteed handling.

Multiple browser tabs: Independent pools in separate tabs do not automatically coordinate with one another. Shared server-side limits may still be needed to control aggregate traffic.

For these reasons, bulkhead isolation works best as one part of a broader reliability strategy that includes timeouts, appropriate error handling, monitoring, server-side capacity controls, and well-designed API behavior.

9. Best Practices for Implementing Bulkhead Isolation

Developers should follow these practices when introducing bulkheads into an AJAX application:

  1. Group requests according to business purpose, criticality, and resource requirements.

  2. Reserve appropriate capacity for essential operations.

  3. Set reasonable concurrency limits based on actual performance measurements.

  4. Limit queue sizes and define what happens when a queue is full.

  5. Use timeouts to prevent requests from waiting indefinitely.

  6. Cancel obsolete requests when they are no longer useful.

  7. Handle HTTP errors, network failures, and timeouts separately where appropriate.

  8. Monitor queue length, request duration, failure rates, and rejected tasks.

  9. Apply server-side resource controls as well as client-side limits.

  10. Test the application under heavy load and simulated service failures.

These practices help ensure that isolation improves reliability rather than simply moving the bottleneck from active requests to a growing queue.

10. Conclusion

AJAX resilience through bulkhead isolation is a software design technique that divides asynchronous requests into independent groups with separate resource limits. It helps prevent slow, failing, or excessive requests in one feature from overwhelming other parts of a web application.

For example, an e-commerce application can isolate recommendation requests from shopping cart updates and product retrieval. If recommendations become slow, the application can continue processing other operations within their allocated limits.

Bulkhead isolation does not eliminate failures, guarantee that every request succeeds, or replace server-side protection. However, when combined with concurrency limits, bounded queues, timeouts, cancellation, monitoring, and appropriate error handling, it provides an effective way to improve the stability and fault tolerance of AJAX-based web applications.