bottleneck

repository·master·Indexed 23 days ago

https://github.com/sgrondin/bottleneck

A highly configurable distributed task scheduler and rate limiter for Node.js and the browser. Version 2.19.5 provides concurrency limits, timing intervals, and reservoir-based burst control to manage job execution. It supports Promises, async/await, and callbacks, and includes features for load shedding strategies, automatic job retries, and limiter chaining.

Tokens
7.6K
Snippets
13
Records
45
Agent score
84%

What's inside bottleneck

  1. Use Refresh and Increase Reservoir Intervals

    master

    Reservoir Intervals allow for bursty execution by automatically managing the reservoir value (the number of jobs allowed to run).

    Refresh Interval

    Resets the reservoir to a specific amount at a fixed interval. Useful for 'X requests per Y seconds' patterns.

    const limiter = new Bottleneck({
      reservoir: 100, // initial value
      reservoirRefreshAmount: 100,
      reservoirRefreshInterval: 60 * 1000, // must be divisible by 250
    
      maxConcurrent: 1,
      minTime: 333
    });

    Increase Interval

    Gradually increments the reservoir by a specific amount at a fixed interval. Useful for APIs with sliding window limits (like Shopify).

    const limiter = new Bottleneck({
      reservoir: 40, // initial value
      reservoirIncreaseAmount: 2,
      reservoirIncreaseInterval: 1000, // must be divisible by 250
      reservoirIncreaseMaximum: 40,
    
      maxConcurrent: 5,
      minTime: 250
    });

    Important Warnings:

    • Always use minTime and maxConcurrent alongside Reservoir Intervals. Without them, a reservoir refresh might trigger a massive burst of jobs all at once.
    • Intervals start at creation. If jobs are added right before a refresh, they might execute immediately after the refresh, effectively doubling the burst.
    • Memory Management: Reservoir intervals prevent garbage collection. Call limiter.disconnect() to clear intervals and free memory.
    // Refresh Interval Example
    const limiter = new Bottleneck({
      reservoir: 100,
      reservoirRefreshAmount: 100,
      reservoirRefreshInterval: 60 * 1000,
      maxConcurrent: 1,
      minTime: 333
    });
    
    // Increase Interval Example
    const limiter = new Bottleneck({
      reservoir: 40,
      reservoirIncreaseAmount: 2,
      reservoirIncreaseInterval: 1000,
      reservoirIncreaseMaximum: 40,
      maxConcurrent: 5,
      minTime: 250
    });
  2. How the Group feature works

    master

    The Group feature manages multiple limiters automatically by creating them dynamically based on a key. This is ideal for scenarios like rate-limiting per origin IP, where you want to apply the same rate-limiting rules to many different entities independently without manually managing thousands of limiter instances.

    When you call .key(str) on a Group, it returns a limiter instance. If a limiter for that specific key doesn't exist, it is created using the options provided to the Group constructor. To prevent memory leaks, idle limiters are automatically deleted after a certain period (default is 5 minutes, configurable via the timeout option in milliseconds).

    const group = new Bottleneck.Group(options);
    
    // Use a key (e.g., an IP address) to route to a specific limiter
    group.key("77.66.54.32").schedule(() => {
      /* process the request */
    });
  3. How to contribute to Bottleneck

    master

    To contribute to the project:

    1. Clone the repository.
    2. Make changes only to files in src/.
    3. Build and test: ./scripts/build.sh && npm test.
    4. For faster development compilation, use: ./scripts/build.sh dev.

    Testing Requirements:

    • Tests must pass in Clustering mode and using the ES5 bundle.
    • Requires a local Redis server. If using non-default host/port, configure them in the .env file.
    • Run full tests with: ./scripts/build.sh && npm run test-all.

    Always ensure you build and test without the dev flag before submitting a Pull Request.

  4. Use Batching to group multiple operations

    master

    The Batcher feature allows you to group multiple requests into a single batch, which is useful for APIs that support bulk operations.

    Note: Batching does not perform throttling; it only optimizes request grouping based on time and size constraints.

    To use it:

    1. Create a new Bottleneck.Batcher(options).
    2. Listen for the "batch" event to receive the grouped items.
    3. Use .add(item) to add items to the batch. This returns a Promise that resolves once the batch has been flushed.
    const batcher = new Bottleneck.Batcher({
      maxTime: 1000,
      maxSize: 10
    });
    
    batcher.on("batch", (batch) => {
      console.log(batch); // e.g., ["some-data", "some-other-data"]
      // Handle batch here
    });
    
    batcher.add("some-data");
    batcherer.add("some-other-data");
  5. Configure new limiters in a Group using on("created")

    master

    The recommended way to configure a newly created limiter within a Group is to listen for the "created" event. This event handler is executed before .key(str) returns the new limiter, allowing you to attach event listeners (like "error") or other configurations to the specific limiter instance.

    group.on("created", (limiter, key) => {
      console.log("A new limiter was created for key: " + key)
    
      // Prepare the limiter, for example we'll want to listen to its "error" events!
      limiter.on("error", (err) => {
        // Handle errors here
      })
    });
  6. Implement automatic job retries

    master

    You can automatically retry failed jobs by listening to the failed event. To trigger a retry, return an integer from the event handler representing the delay in milliseconds before the next attempt.

    Important: Retried jobs stay in the EXECUTING state while waiting. They count towards maxConcurrent during the wait period.

    const limiter = new Bottleneck();
    
    limiter.on("failed", async (error, jobInfo) => {
      const id = jobInfo.options.id;
      
      // Retry once after 25ms
      if (jobInfo.retryCount === 0) {
        return 25;
      }
    });
    
    // To retry immediately, return 0
    const limiter = new Bottleneck();
    
    limiter.on("failed", async (error, jobInfo) => {
      const id = jobInfo.options.id;
      console.warn(`Job ${id} failed: ${error}`);
    
      if (jobInfo.retryCount === 0) {
        console.log(`Retrying job ${id} in 25ms!`);
        return 25;
      }
    });
    
    const result = await limiter.schedule({ id: 'ABC123' }, async () => {
      // ... logic
    });
  7. How to use Bottleneck with Promises, Async/Await, and Callbacks

    master

    Bottleneck provides different ways to schedule tasks depending on your coding style.

    Promises

    Wrap your function call in limiter.schedule():

    limiter.schedule(() => myFunction(arg1, arg2))
      .then((result) => { /* handle result */ });

    Alternatively, use limiter.wrap() to create a new function that is automatically rate-limited:

    const wrapped = limiter.wrap(myFunction);
    
    wrapped(arg1, arg2)
      .then((result) => { /* handle result */ });

    Async/Await

    Use limiter.schedule() with await:

    const result = await limiter.schedule(() => myFunction(arg1, arg2));

    Or use a wrapped function:

    const wrapped = limiter.wrap(myFunction);
    const result = await wrapped(arg1, arg2);

    Callbacks

    Use limiter.submit() for traditional callback-style functions:

    limiter.submit(someAsyncCall, arg1, arg2, callback);
    const limiter = new Bottleneck({
      minTime: 333
    });
    
    // Using schedule with promises
    limiter.schedule(() => myFunction(arg1, arg2))
    .then((result) => {
      /* handle result */
    });
  8. Debug your application with Bottleneck

    master

    Debugging complex scheduling logic (priorities, weights, latency) can be difficult. Use the following strategies to troubleshoot:

    1. Catch Errors: Listen for the "error" event emitted by limiters and Groups. These are often uncaught exceptions from your application code.
    2. Real-time Monitoring: Listen to the "debug" event to see detailed information about how the limiter is executing code. For better readability, include job IDs in your jobs.
    3. Identify Bottleneck Errors: Bottleneck uses BottleneckError objects to signal when it has to fail a job. You can distinguish these from your own application errors using instanceof.

    Always review the 'Gotchas' section if behavior is unexpected.

    limiter.schedule(fn)
    .then((result) => { /* ... */ } )
    .catch((error) => {
      if (error instanceof Bottleneck.BottleneckError) {
        /* ... */
      }
    });
  9. Install Bottleneck

    master

    Install Bottleneck via npm for Node.js or the browser.

    npm install --save bottleneck

    If you need to support older browsers or Node versions earlier than 6.0, use the ES5 bundle:

    var Bottleneck = require("bottleneck/es5");
  10. Migrate from Bottleneck v1 to v2

    master

    If you are upgrading from v1 to v2, note the following breaking changes:

    Environment & Compatibility

    • Node/Browser: Requires Node 6+ or a modern browser. For ES5 support, use require("bottleneck/es5").

    Constructor & Configuration

    • Options Object: The Bottleneck constructor and the Group constructor now both take an options object.
    • Unlimited Values: Use null instead of 0 for maxConcurrent and null instead of -1 for highWater to indicate unlimited values.
    • Settings Updates: changeSettings() is renamed to updateSettings(). It now returns a promise and accepts the same options object as the constructor.
    • Promise Libraries: Changing the Promise library is now handled via the options object.

    API Renames & Removals

    • Groups: The Cluster feature is now called Group.
    • Job Submission:
      • Use submit() with an options object instead of submitPriority().
      • Use schedule() with an options object instead of schedulePriority().
    • Status Methods:
      • nbQueued() $\rightarrow$ queued()
      • nbRunning $\rightarrow$ running() (now returns a promise).
    • Removals:
      • isBlocked()
      • changePenalty() (use options object)
      • changeReservoir() (use options object)
      • stopAll() (use stop())
      • Group.changeTimeout() (pass timeout in Group options)

    Defaults

    • rejectOnDrop is now true by default. Set to false to retain v1 behavior (though enabling it is considered poor practice).
  11. Quick Start: Basic Rate Limiting

    master

    To implement basic rate limiting, use the Bottleneck constructor with minTime and/or maxConcurrent options.

    • minTime: The minimum amount of time (in milliseconds) between the start of each task. For example, 333 ms allows approximately 3 requests per second.
    • maxConcurrent: The maximum number of tasks that can run at the same time. Setting this to 1 ensures that only one request is active at any given moment, preventing overlapping requests if they take longer than the minTime interval.
    // Execute 3 requests per second
    const limiter = new Bottleneck({
      minTime: 333
    });
    
    // Execute 3 requests per second, but ensure only 1 runs at a time
    const limiter = new Bottleneck({
      maxConcurrent: 1,
      minTime: 333
    });
    const limiter = new Bottleneck({
      minTime: 333
    });
  12. Configure overflow strategies

    master

    When the limiter reaches its highWater (High Water Mark) limit, it uses a strategy to decide what to do with new jobs. These strategies are available via Bottleneck.strategy:

    • LEAK (1): Drops the oldest job in the queue to make room for the new one.
    • OVERFLOW (2): Drops the new job being submitted.
    • OVERFLOW_PRIORITY (4): Drops the job with the lowest priority in the queue to make room for the new one.
    • BLOCK (3): (Note: Implementation details for BLOCK may vary, but it is a defined strategy constant).