Serving Tasks Efficiently: Understanding P-Limit In Javascript

Chidozie C. Okafor
4 min readOct 18, 2023

--

You are at a busy restaurant. There’s just so many tables available, and there’s a large queue of people wanting to be seated. The people at the restaurant serve as tasks for the JavaScript programme, which is represented by the programm.

Let’s imagine that this restaurant has a policy stating that a set number of people may be seated at once. Others must wait in queue until a seat becomes available. This is comparable to the operation of the JavaScript “p-limit” library. The number of promises (tasks) that can run concurrently is limited.

Why would we need this?

When too many people are seated at once at a restaurant, the staff may feel overworked and the service may suffer. Similar to this, trying to run too many tasks at once in a programme might cause it to lag or even crash. This is particularly crucial for resource-intensive tasks like file system access and network request processing.

You can regulate the flow of tasks to guarantee that only a predetermined number can run concurrently by using p-limit. By doing this, you can guarantee that your programme will always be responsive and effective.

How does it work?

Assume there is a unique gatekeeper at the eatery. Only a limited number of persons are let in at once by this gatekeeper, who is aware of how many tables are available. When one set of persons departs, the gatekeeper lets the next set in.

This gatekeeper in “p-limit” is a function you define that sets a limit on how many promises can execute concurrently.

Let’s see some code!

First, you need to install the p-limit library:

yarn add p-limit

Next, let’s write some code:

const pLimit = require('p-limit');

// This creates a gatekeeper that only allows 2 promises to run at once
const limit = pLimit(2);

const cookDish = async (dishName) => {
// Simulating a time-consuming task
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`${dishName} is ready!`);
};

// Create an array of dishes to be cooked
const dishes = ['Pizza', 'Burger', 'Pasta', 'Salad', 'Ice Cream'];

// This is like the customers waiting in line
const tasks = dishes.map(dish => {
return limit(() => cookDish(dish));
});

// Execute all tasks
Promise.all(tasks).then(() => {
console.log('All dishes are served!');
});

even though we have five dishes, only two will be cooked at the same time due to our limit. So, you’ll see:

Pizza is ready!
Burger is ready!
Pasta is ready!
... and so on.

But remember, only two dishes are being cooked simultaneously!

HubHub Youtube Fetcher

Now let’s look at an example from Hubbub, which helps to further understand it.

A feature at Hubbub retrieves data from a YouTube channel, including the various video shelves (categories of videos) and the videos contained in those shelves.

But you can’t just send a tonne of queries to YouTube’s servers in a short amount of time because they have rate constraints. They will temporarily block you if you do. This is the sweet spot for “p-limit”.

Here’s how we use it at Hubbub:

const pLimit require('p-limit');


const limit = pLimit(5);

async getYoutubeChannelItemList(channelId) {
try {
console.log('channelId', channelId);
const response = await youtube.getChannel(channelId);
const allShelfItems = [];

for (const shelf of response.shelves) {
const shelfItemsPromises = shelf.items.map(item => {
// This is the crucial part. For each item in the shelf, we limit how many can be processed simultaneously.
return limit(() => this.createItemFromVideo(item, response, channelId, 'youtubeChannels'));
});

// Wait for all the video items in this shelf to be processed
const shelfItems = await Promise.all(shelfItemsPromises);
allShelfItems.push(...shelfItems);
}

return allShelfItems;
} catch (error) {
Sentry.captureException(error); // Reporting the error to an error tracking platform
throw new HttpException(INTERNAL_SERVER_ERROR, error.message); // Handle the error gracefully
}
}

Breaking it Down

  1. Set Up the Limit: pLimit(5) means at any given time, a maximum of 5 promises (tasks) are running concurrently. Think of it as only allowing 5 YouTube video fetch requests at the same time.
  2. Fetch the Channel: youtube.getChannel(channelId) fetches the YouTube channel's details, including its shelves.
  3. Process Each Shelf: For each shelf in the channel, We want to process the video items. But instead of processing all items at once and risking a rate limit violation, it uses our limit:
  4. return limit(() => this.createItemFromVideo(item, response, channelId, 'youtubeChannels'));
  5. Here, the createItemFromVideo function is called, but only 5 of them will run at the same time.
  6. Wait for Completion: await Promise.all(shelfItemsPromises) ensures that the code waits until all video items in the current shelf are processed before moving on to the next shelf.

We makes sure we collect YouTube channel details quickly and without going over YouTube’s rate constraints by using p-limit. It’s a great illustration of how to effectively handle several asynchronous processes. A well-designed programme handles its responsibilities optimally, just as a restaurant offers excellent service by managing its people!

--

--

Chidozie C. Okafor
Chidozie C. Okafor

Written by Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC

No responses yet