@clickup/distributed-pacer

中文文档
2.10.296发布于 2024/08/24MITTypeScriptCommonJS近 30 天下载 5259

A concurrency aware Redis-backed rate limiter with pacing delay prediction and Token Bucket bursts handling.

此软件包已被标记为弃用

This package is no longer maintained

原始使用文档
文档目录7 节
  1. Pacing
  2. Pacing Usage Example
  3. How it Works
  4. Bursts Allowance
  5. Rate Limiting
  6. Rate Limiting Usage Example
  7. Performance

distributed-pacer: A concurrency aware Redis-backed rate limiter with pacing delay prediction and Token Bucket bursts handling

See also Full API documentation.

CI run

Pacing

Pacing controls the rate at which concurrent clients perform some operations. It introduces deliberate delays between client requests. The primary goal of pacing is to ensure that the rate of operations (such as outgoing requests) does not exceed a certain threshold (e.g. QPS - "queries per second").

Use Cases:

  • Outgoing Requests. Pacing is typically used by clients to manage the rate at which they send requests to an external service that imposes rate limits.
  • Load Management. In scenarios where the external service might be sensitive to sudden spikes in traffic, pacing helps in distributing the load more evenly over time.

Notice that the term "pacing" is typically used for outgoing requests from clients (to slow down the requests flow without dropping them), whilst "rate limiting" is for incoming requests on servers (to reject non-conforming requests).

Pacing Usage Example

import { DistributedPacer } from "@clickup/distributed-pacer";
import { Redis } from "ioredis";
import { setTimeout } from "timers/promises";

const myIoRedis = new Redis();

async function mySendApiRequest() {
  // sends an API request somewhere
}

async function myWorkerRunningOnMultipleMachines() {
  while (true) {
    const lightweightPacer = new DistributedPacer(myIoRedis, {
      key: "myKey",
      qps: 10,
      maxBurst: 1, // optional
      burstAllowanceFactor: 0.5, // optional
    });
    const outcome = await lightweightPacer.pace(1 /* weight */);

    console.log(outcome.reason);
    await setTimeout(outcome.delayMs);

    await mySendApiRequest();
  }
}

How it Works

DistributedPacer spreads the requests issued by some concurrent workers or processes uniformly into the future to satisfy the desired downstream QPS (queries per second) exactly. The implementation is inspired by Leaky Bucket for Queues algorithm.

The general use case is to introduce some artificial back-pressure when sending requests to external services, to avoid overloading them, e.g.:

  • Pacing outgoing requests to some external API to meet its rate limits.
  • Protecting the local database from overloading with concurrent writes done by multiple workers.

Imagine we have a time machine, and we can send requests (events) into the exact provided moment of time in the future. To send a request into the future, the Lua script in Redis returns that moment's timestamp, and then the worker needs to call delay() to wake up at that moment. We also store the last moment of the future to where we sent a previous request, so next requests coming (if they come too quickly) will be sent further and further away.

Another analogy is booking a meeting in the calendar. When a new request arrives, it's not executed immediately, but instead scheduled in the calendar according to the QPS allowance.

Thus, after the returned delayMs is awaited, the request will happen in at least 1/QPS seconds after the previous request; thus, it will satisfy the target QPS. Also, if there were no requests in the past within 1/QPS seconds from the present time, then delayMs returned will be 0.

Bursts Allowance

Imagine that each call to pace(weight) adds weight of water to the bucket of maxBurst volume, and every second, 1/qps*burstAllowanceFactor of water leaks out of the bucket at a constant rate (but only when the pacer is idle, i.e. there are no requests scheduled to the future on top of the bucket). If the bucket is not yet full (its watermark is below maxBurst level), then the returned delayMs will be 0, so the worker can proceed with the request immediately. Otherwise, pacing will start to happen. I.e. we pace only the requests which cause the bucket to overflow (Leaky Bucket algorithm).

The default value of burstAllowanceFactor is less than 1, which forces the burst allowance to be earned slightly slower than the target QPS.

Rate Limiting

Although pacing is the primary use case for this module, it also supports "rate limiting" mode, where it's expected that requests out of quota will be rejected (instead of being delayed). This is useful for handling incoming requests on servers (as opposed to pacing, where the requests originate from workers).

To use DistributedPacer in rate limiting mode, call rateLimit() method on it. Logically, it works exactly the same way as pace(), but when it returns a non-zero delayMs, it doesn't alter the state in Redis, assuming that the request will be rejected and won't contribute to maxBurst allowance.

To utilize the power of Leaky Bucket algorithm (or its equivalent here, Token Bucket), pass a nonzero value to maxBurst. With the default value (which is 0), no bursts will be allowed, so the requests will need to come in not less than 1/qps seconds in between.

Rate Limiting Usage Example

Disclaimer: here, we use Express just as an illustration: there is obviously a ready middleware module for Express rate limiting use case. Use DistributedPacer in other applications, like GraphQL processing, WebSockets, internal IO services etc.

import { DistributedPacer } from "@clickup/distributed-pacer";
import { Redis } from "ioredis";
import express from "express";

const myIoRedis = new Redis();

express()
  .get('/', (req, res) => {
    const lightweightPacer = new DistributedPacer(myIoRedis, {
      key: "myKey",
      qps: 10,
      maxBurst: 20,
    });
    const outcome = await lightweightPacer.rateLimit(1 /* weight */);

    if (outcome.delay > 0) {
      console.log(outcome.reason);
      res.status(429).send(`Rate limited, try again in ${outcome.delay} ms.`);
    } else {
      res.send("Hello World!")
    }
  })
  .listen(port);

Performance

This module is cheap and can be put on a critical path in your application.

DistributedPacer objects are lightweight, so you can create them as often as you want (even on every request).

Each call to pace() or rateLimit() causes one round-trip to Redis (it runs a custom Lua function), and the timing of that call is O(1).

If you need to use multiple keys, you can use Redis in cluster mode to spread those keys across multiple Redis nodes (pass an instance of Redis.Cluster to DistributedPacer constructor).

版本与发布记录

@clickup/distributed-pacer 共发布 1 个版本,首次发布于 2024/08/24;过去 12 个月发布了 0 个版本。

版本发布日期距今
2.10.296latest2024/08/242 年前

关于 @clickup/distributed-pacer 的常见问题

@clickup/distributed-pacer 是什么?

A concurrency aware Redis-backed rate limiter with pacing delay prediction and Token Bucket bursts handling.注意:该包已被作者标记为弃用——This package is no longer maintained

如何安装 @clickup/distributed-pacer?

在项目目录运行 npm install @clickup/distributed-pacer,或使用 pnpm add / yarn add / bun add。

@clickup/distributed-pacer 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@clickup/distributed-pacer 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@clickup/distributed-pacer 可以商用吗?

@clickup/distributed-pacer 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@clickup/distributed-pacer 的下载量有多少?

根据 npm 官方下载统计,@clickup/distributed-pacer 在 2026/10/10 – 2026/11/08 这 30 天内被下载 5,259 次,日均约 175 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@clickup/distributed-pacer 还在维护吗?

最新版本 2.10.296 发布于 2024/08/24(2 年前);至今共发布 1 个版本,其中过去 12 个月发布了 0 个;首次发布于 2024/08/24。作者已将其标记为弃用,新项目建议寻找替代方案。