Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Batching Network Requests

Recommended resources

Hello! Welcome to the final lesson in our module on Memory Management and Performance Foundations.

In our previous lessons, we've focused on making individual network operations more efficient—first by preventing unnecessary requests with debouncing, and then by speeding up how we process the data with optimized Codable parsing. Now, we'll tackle a different but equally important performance problem: the overhead of making too many small requests.

This lesson addresses the learning outcome: Implement request batching to group multiple small network calls into a single request. We will explore why making many small requests is inefficient and learn two primary strategies for grouping them: a client-side approach you can implement immediately, and a server-side approach that requires API support.

The "Why": The High Cost of Chatty Apps

Imagine a social feed where you need to display posts from 10 different users. A naive implementation might make one request to get the posts, and then 10 more requests to fetch the profile information for each user. This is often called the "N+1" problem. While each individual request might be fast, the cumulative effect can be disastrous for performance and battery life.

Each network request carries significant overhead:

  • Connection Setup: Establishing a TCP connection involves a multi-step handshake.
  • HTTP Overhead: Headers can add hundreds of bytes to every request and response.
  • Radio Power-Up: On a mobile device, the cellular or Wi-Fi radio must power up to a high-energy state to transmit, consuming significant battery. It then stays in this state for a short period, even after the request is complete. Many small requests keep the radio active far longer than necessary.

Apple's own documentation emphasizes the importance of bundling work together to conserve energy.

Energy Efficiency Guide for iOS Apps: Defer Networking

In the Energy Efficiency Guide for iOS Apps, Apple explains why batching network transactions is a critical optimization for saving power.

Read the section 'Batch Transactions'. Note the examples given, like downloading multiple ads or emails at once, to understand the principle.

The core idea is to make fewer, larger requests instead of many small ones.

Visualizing Request Batching
This image visually contrasts the inefficiency of multiple individual network calls (left) with the efficiency of a single, batched request (right). The red wall symbolizes the bottleneck created by many small requests, while the green stack represents the streamlined process of batching.

The article "Adding batch or bulk endpoints to your REST API" further explains why the number of network calls is often the primary bottleneck in modern applications.

Adding batch or bulk endpoints to your REST API - Codementor

To solidify your understanding of the problem, let's read a short section from this article on Codementor.

Read the section titled 'Why would we need batch endpoints?'. This reinforces the concept that network latency, not just bandwidth, is a major performance factor.

Now that we understand the problem, let's explore the solutions.


Strategy 1: Client-Side Coordination with DispatchGroup

Even if the backend API doesn't offer a way to fetch multiple resources at once, you can still improve the user experience by coordinating requests on the client. The goal is to fire all necessary requests in parallel and wait for all of them to complete before updating the UI. This prevents multiple, staggered UI updates and provides a single, coherent result to the user.

The classic tool for this in Grand Central Dispatch (GCD) is DispatchGroup. A DispatchGroup allows you to track the completion of a group of asynchronous tasks without blocking the main thread.

The basic pattern is:

  1. Create a DispatchGroup.
  2. Call group.enter() for each asynchronous task you're about to start.
  3. Call group.leave() inside the completion handler of each task.
  4. Use group.notify(queue:) to schedule a block of code that will run only after the number of leave calls matches the number of enter calls.

This video provides an excellent practical demonstration of using DispatchGroup.

Grouping network calls like a boss - DispatchGroup

The 'Grouping network calls like a boss' video by Swift Arcade clearly explains and demonstrates how to use DispatchGroup for coordinating network calls.

Watch from the beginning to 07:41. Pay close attention to the pairing of enter() and leave() calls and how the notify() block is used to update the UI only when all data has been fetched. This covers the most common use case for DispatchGroup.

Synchronous Waiting with wait()

DispatchGroup also offers a wait() method, which synchronously blocks the current thread until all tasks in the group have completed. This can be useful, but it's extremely dangerous if used incorrectly.

Never call wait() on the main thread. Doing so will freeze your entire user interface.

The correct way to use wait() is to dispatch your work to a background queue, call wait() on that background queue, and then dispatch the result back to the main queue for any UI updates.

Let's continue with the same video to see how this works and what happens when you get it wrong.

Grouping network calls like a boss - DispatchGroup

This next segment demonstrates the wait() method and its major pitfall.

Watch from 07:41 to the end (13:31). The video shows a valid use case for wait() (checking for duplicates before sending money) and critically demonstrates why you must wrap the call in a background queue to avoid blocking the main thread.


Strategy 2: Server-Side Batching (Bulk/Batch APIs)

Client-side coordination is good, but true batching happens when the server provides a dedicated endpoint for it. This is far more efficient as it reduces N requests to just 1, minimizing all the overhead we discussed earlier.

There are two common architectural patterns for batching on the server side. As a professional developer, understanding these API design patterns is crucial for when you're collaborating with backend teams or designing APIs yourself.

The Codementor article we looked at earlier provides a great breakdown of these patterns.

Adding batch or bulk endpoints to your REST API - Codementor

Let's explore the two main server-side batching strategies, using examples from Google Drive and ZenDesk.

Read the sections 'Google Drive - batching requests' and 'ZenDesk - batching resources'. Contrast the two approaches: Google's flexible but complex 'meta-request' endpoint versus ZenDesk's simpler, resource-specific bulk endpoints.

To summarize the trade-offs:

Pattern Description Pros Cons
Generic Batch Endpoint A single endpoint (e.g., /batch) accepts a list of different, independent HTTP requests. Highly flexible. Can combine different operations (e.g., create a user, update a document) in one call. Complex to construct on the client and for the server to parse.
Resource-Specific Bulk Endpoint Multiple endpoints exist for bulk operations on a single resource type (e.g., /api/v2/users/create_many). Simple to use and implement. A natural extension of single-resource endpoints. Less flexible. Cannot combine operations on different resource types in a single call.

When implementing or using such APIs, there are other important factors to consider.

Adding batch or bulk endpoints to your REST API - Codementor

Finally, let's review some key implementation details for batch endpoints.

Read the section 'Other considerations for batch processing'. Pay attention to the points about error handling (all-or-nothing vs. partial success) and limiting batch sizes.


A Note on Modern Concurrency: TaskGroup

While DispatchGroup is a powerful and widely used tool from the GCD era, it's worth knowing its modern equivalent in Swift's structured concurrency: TaskGroup.

A TaskGroup serves the same purpose—running multiple parallel, dynamic tasks—but with the benefits of async/await syntax and cancellation propagation.

Here’s a conceptual comparison:

DispatchGroup (GCD)

func fetchAllUserDetails(for userIDs: [String], completion: @escaping ([User]) -> Void) {
    let group = DispatchGroup()
    var users: [User] = []
    let lock = NSLock() // Protect shared mutable state

    for id in userIDs {
        group.enter()
        fetchUser(id: id) { user in
            if let user = user {
                lock.lock()
                users.append(user)
                lock.unlock()
            }
            group.leave()
        }
    }

    group.notify(queue: .main) {
        completion(users)
    }
}

TaskGroup (Swift Concurrency)

func fetchAllUserDetails(for userIDs: [String]) async -> [User] {
    await withTaskGroup(of: User?.self) { group in
        for id in userIDs {
            group.addTask {
                return await fetchUser(id: id) // Assumes fetchUser is an async func
            }
        }

        var users: [User] = []
        for await user in group {
            if let user = user {
                users.append(user)
            }
        }
        return users
    }
}

Notice how TaskGroup eliminates the need for manual locking and completion handlers, resulting in cleaner, safer code. As you continue to adopt modern Swift, TaskGroup will be your go-to tool for client-side request coordination.


Conclusion

This lesson concludes our module on performance optimization. You've learned to look beyond simply making network calls and to consider their efficiency and impact on the system.

Key Takeaways:

  • Batching is Crucial: Making many small network requests is inefficient due to connection, HTTP, and radio overhead. Grouping them into fewer, larger requests improves performance and saves battery.
  • Client-Side Coordination: Use DispatchGroup (or TaskGroup in modern Swift) to run multiple requests in parallel and act only when all have completed. Remember to never block the main thread with wait().
  • Server-Side Batching: The most efficient solution involves a dedicated batch/bulk API. Understand the trade-offs between a generic batch endpoint (flexible, complex) and resource-specific bulk endpoints (simple, less flexible).
  • Consider Edge Cases: Professional-grade batching requires clear strategies for error handling (partial vs. total failure) and rate limiting.

Preview of the Next Module:

We are now wrapping up our deep dive into low-level performance and memory management. You have built a strong foundation in how to write efficient, responsive code.

In our next module, Modern Architecture with MVVM, we will move up a level of abstraction. We'll start by exploring how to structure your app's code for better testability, scalability, and separation of concerns. Our first lesson will focus on explaining the separation of concerns in MVVM and its advantages over traditional MVC. This will be your first step toward mastering the architectural patterns used in modern, professional iOS development.