p-throttle

repository·main·Indexed 19 days ago

https://github.com/sindresorhus/p-throttle

A utility for rate-limiting promise-returning and async functions. Unlike standard throttling, p-throttle queues all calls to ensure no executions are lost, making it ideal for interacting with rate-limited external APIs. It supports configurable limits and intervals, weighted cost-based rate limiting, AbortSignal for canceling pending executions, and monitoring via the onDelay callback and queueSize property.

Tokens
3.8K
Snippets
13
Records
14
Agent score
64%

What's inside p-throttle

  1. How p-throttle works

    main

    p-throttle rate-limits function calls without discarding them. Unlike debouncing or throttling that might drop calls, p-throttle queues all calls and executes them sequentially or according to the specified limit. This ensures that every call is eventually executed with its original context and arguments preserved, making it ideal for external API interactions where call loss is unacceptable.

    It works with both async/promise-returning functions and normal functions.

    import pThrottle from 'p-throttle';
    
    const throttle = pThrottle({
    	limit: 2,
    	interval: 1000
    });
    
    const throttled = throttle(async index => {
    	return `${index}: processed`;
    });
    
    // All calls are queued and executed at the specified rate
    for (let index = 1; index <= 6; index++) {
    	(async () => {
    		console.log(await throttled(index));
    	})();
    }
  2. Manage queue size and throttling state

    main

    The ThrottledFunction returned by pThrottle includes two additional properties to help manage execution:

    • isEnabled: A boolean indicating whether future function calls should be throttled and count towards thresholds. Defaults to true.
    • queueSize: A read-only number representing the number of queued items waiting to be executed. This is useful for implementing fallback strategies (e.g., using a different API if the queue is too full).
    import pThrottle from 'p-throttle';
    
    const throttle = pThrottle({limit: 1, interval: 1000});
    
    const accurateData = throttle(() => fetch('https://accurate-api.example.com'));
    const roughData = () => fetch('https://rough-api.example.com');
    
    async function getData() {
    	if (accurateData.queueSize >= 3) {
    		return roughData(); // Queue full, use fallback
    	}
    
    	return accurateData();
    }
  3. Configure weighted throttling

    main

    You can assign different 'costs' to different function calls by providing a weight function. This is useful when some operations are more resource-intensive than others.

    Constraints:

    • The weight option cannot be used if interval is 0.
    • The value returned by weight must be a finite non-negative number.
    • The value returned by weight must be less than or equal to the limit.
    const throttled = pThrottle({
    	limit: 5,
    	interval: 1000,
    	weight: (arg) => (arg === 'heavy' ? 3 : 1)
    });
    
    const throttledFn = throttled(async (type) => {
    	// 'heavy' calls consume 3 units of the limit, others consume 1
    	return type;
    });
  4. Use the onDelay callback to monitor throttling

    main

    The onDelay option allows you to monitor when calls are being queued due to rate limits. The callback is passed the arguments of the call that was delayed.

    import pThrottle from 'p-throttle';
    
    const throttle = pThrottle({
    	limit: 2,
    	interval: 1000,
    	onDelay: (a, b) => {
    		console.log(`Reached interval limit, call is delayed for ${a} ${b}`);
    	},
    });
    
    const throttled = throttle((a, b) => {
    	console.log(`Executing with ${a} ${b}...`);
    });
    
    await throttled(1, 2);
    await throttled(3, 4);
    await throttled(5, 6); // This triggers onDelay
    //=> Executing with 1 2...
    //=> Executing with 3 4...
    //=> Reached interval limit, call is delayed for 5 6
    //=> Executing with 5 6...
  5. Abort pending executions with AbortSignal

    main

    You can pass an AbortSignal to the pThrottle options. When the signal is aborted, all pending (unresolved) promises in the queue will be rejected with the signal.reason.

    import pThrottle from 'p-throttle';
    
    const controller = new AbortController();
    
    const throttle = pThrottle({
    	limit: 2,
    	interval: 1000,
    	signal: controller.signal
    });
    
    const throttled = throttle(() => {
    	console.log('Executing...');
    });
    
    await throttled();
    await throttled();
    controller.abort('aborted');
    
    // This call will be rejected
    await throttled();
    //=> Promise rejected with reason `aborted`
  6. Manage the throttled function queue and state

    main

    The object returned by pThrottle (the throttledFn) provides properties to inspect and control the throttling state:

    • throttledFn.isEnabled (boolean): Indicates whether future calls will be subject to throttling. Defaults to true.
    • throttledFn.queueSize (number): Returns the number of items currently waiting in the queue. This is useful for implementing fallback logic when the queue becomes too large.
    import pThrottle from 'p-throttle';
    
    const throttle = pThrottle({limit: 1, interval: 1000});
    const accurateData = throttle(() => fetch('https://accurate-api.example.com'));
    const roughData = () => fetch('https://rough-api.example.com');
    
    async function getData() {
    	if (accurateData.queueSize >= 3) {
    		return roughData(); // Use fallback if queue is too full
    	}
    
    	return accurateData();
    }
  7. Configure pThrottle(options)

    main

    The pThrottle function accepts an options object to define the rate-limiting behavior. Both limit and interval are required.

    OptionTypeDefaultDescription
    limitnumberRequiredThe maximum number of calls allowed within the interval
    intervalnumberRequiredThe timespan for the limit in milliseconds
    strictbooleanfalseIf true, uses a resource-intensive algorithm that throttles each call individually to ensure the limit is never exceeded in any interval. The default uses a windowed approach.
    signalAbortSignalundefinedAn AbortSignal to abort pending executions. Unresolved promises will be rejected with signal.reason
    onDelayFunctionundefinedA callback triggered when a call is delayed due to exceeding the limit. Receives the delayed call's arguments.
    weightFunctionargs => 1A function to calculate the 'cost' of a call based on its arguments. This determines how much of the limit is consumed.
    import pThrottle from 'p-throttle';
    
    // Example: Using weight to handle cost-based API limits
    const throttle = pThrottle({
    	limit: 100,
    	interval: 1000,
    	weight: numberOfTables => 1 + numberOfTables
    });
    
    const fetchData = throttle(numberOfTables => {
    	return fetch('...');
    });
    
    await fetchData(1); // Consumes 2 points
    await fetchData(3); // Consumes 4 points
  8. Configure p-throttle options

    main

    When calling pThrottle(options), you can configure the following settings:

    • limit (required): The maximum number of calls allowed within the interval.
    • interval (required): The timespan for the limit in milliseconds.
    • strict (optional, default: false): If true, uses a more resource-intensive algorithm that throttles each call individually to ensure the limit is never exceeded for any interval. The default uses a windowed approach.
    • signal (optional): An AbortSignal to abort pending executions. When aborted, all unresolved promises are rejected with signal.reason.
    • onDelay (optional): A callback function triggered when a call is delayed due to exceeding the limit. It receives the arguments passed to the throttled function.
    • weight (optional): A function that calculates the 'cost' of a call based on its arguments. This determines how much of the limit is consumed. Defaults to 1 per call.

    Note: limit and interval must both be specified.

    const throttle = pThrottle({
    	limit: 100,
    	interval: 1000,
    	strict: true,
    	signal: controller.signal,
    	onDelay: (a, b) => { /* ... */ },
    	weight: (args) => { /* ... */ }
    });
  9. Use weight to implement cost-based rate limiting

    main

    If an API uses a point-based or cost-based quota (where different operations consume different amounts of the limit), use the weight option. The weight function receives the arguments of the throttled function and returns a number representing the cost of that specific call.

    import pThrottle from 'p-throttle';
    
    // Storyblok GraphQL API: 100 points per second
    // Each query costs 1 point for the connection plus 1 point per table
    const throttle = pThrottle({
    	limit: 100,
    	interval: 1000,
    	weight: numberOfTables => 1 + numberOfTables
    });
    
    const fetchData = throttle(numberOfTables => {
    	// Fetch GraphQL data
    	return fetch('...');
    });
    
    await fetchData(1); // Costs 2 points
    await fetchData(3); // Costs 4 points
  10. Check throttledFn.isEnabled and throttledFn.queueSize

    main

    The throttled function returned by pThrottle has two additional properties:

    • isEnabled: A boolean indicating if the throttling is active. If false, the function executes immediately without throttling.
    • queueSize: A number representing the current number of requests waiting in the throttle queue.
  11. Use AbortSignal to cancel queued requests

    main

    If you provide an AbortSignal in the pThrottle options, any requests currently waiting in the throttle queue will be rejected with the signal's reason if the signal aborts. This also resets the internal throttling state (like windowed counters) so that subsequent calls are not artificially delayed by the aborted requests.

    import pThrottle from 'p-throttle';
    
    const controller = new AbortController();
    const throttled = pThrottle({
    	limit: 1,
    	interval: 1000,
    	signal: controller.signal
    });
    
    const throttledFn = throttled(async () => {
    	return 'done';
    });
    
    // This call will be queued
    const promise = throttledFn();
    
    // Abort the signal to reject the queued promise
    controller.abort();