npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flarecart/rate-limit

v1.0.0

Published

Sliding-window rate limits with an atomic store (memory or Cloudflare D1), named rules, a circuit breaker for total load, and Cloudflare's rate-limiting bindings. The JS twin of SugarCommerce's PHP rate-limit.

Readme

@flarecart/rate-limit

Rate limits for a Worker (or any JavaScript server): a sliding window over an atomic counter store, limits named in one table, a circuit breaker that caps total load, and Cloudflare's own rate-limiting bindings. Counters live in memory or in Cloudflare D1.

The JavaScript twin of SugarCommerce's PHP rate-limit: the same for(action, identity), Rules and Decision.

npm install @flarecart/rate-limit

Why

Every limiter looks right in review. The ways they fail don't show until the burst they exist to stop:

  • Read-then-write undercounts. Two requests read 9, both write 10, and a limit of 10 lets 11 through. Here a store's increment is one atomic operation (on D1, a single upsert that returns the new count), and the decision is made on the number it returns.
  • A key without an identity is a self-inflicted outage. One counter for "login" lets anyone lock everybody out. for(action, identity) refuses an empty identity, and hashes the one it gets, since an email or IP has no business in a counter's key.
  • Fixed windows double the limit. Spend it at 0:59, again at 1:01. The window here slides: the previous one counts in proportion to how much of it is still in view.
  • Per-caller limits don't stop a spread-out flood. A thousand addresses, a few requests each, all under every limit. CircuitBreaker caps the total and pauses, so an expensive operation has a ceiling.

Features

  • 🪟 Sliding window, decided on an atomic count
  • 🪪 Identity required and hashed: for('login', ip)
  • 📋 Rules: every limit in one table; unknown actions and empty keys are unlimited
  • 🔌 Circuit breaker: total threshold, pause, a fresh count after
  • 🌍 Edge bindings: Cloudflare ratelimits, with fallback to a general binding
  • 🗄️ Stores: memory, or D1 (schema and prune() included)
  • 📨 Decision.headers(): X-RateLimit-* and Retry-After

Usage

import { RateLimiter, Rules, CircuitBreaker, D1Store, checkBinding, firstBinding } from '@flarecart/rate-limit';

const store = new D1Store(env.DB);
const limiter = new RateLimiter(store);

// One limit, inline.
const login = await limiter.for('login', ip).check(5, 60);
if (login.denied) return new Response('Too many attempts.', { status: 429, headers: login.headers() });

// Every limit, in one place.
const rules = new Rules(limiter, { 'sign-in-link': [5, 3600], checkout: [15, 3600] });
if ((await rules.check('sign-in-link', email)).denied) throw tooMany();

// A ceiling on the total: 200 checkouts an hour, then pause for 30 minutes.
const breaker = new CircuitBreaker(store, { name: 'checkout', threshold: 200, window: 3600, pause: 1800 });
const total = await breaker.check();
if (total.denied) return busy(total.retryAfter);

// Cloudflare's binding: per kind of endpoint, falling back to a general one.
const edge = await checkBinding(firstBinding(env.CHECKOUT_LIMITER, env.LIMITER), `checkout:${ipHash}`);

The D1 table, for a migration:

import { d1Schema } from '@flarecart/rate-limit';
console.log(d1Schema()); // CREATE TABLE IF NOT EXISTS rate_limits (…)

API

| | | |---|---| | new RateLimiter(store, { now? }) | for(action, identity) → Limit; check(key, limit, seconds), peek(…), clear(key, seconds). | | Limit | check(limit, seconds), peek(limit, seconds), clear(seconds), key(). | | new Rules(limiter, { action: [limit, seconds] }) | check(action, key), peek, allows, has(action). | | Decision | allowed, denied, limit, remaining, retryAfter (s), resetsAt (unix s), limited, headers(), Decision.unlimited(). | | new CircuitBreaker(store, { name, threshold, window, pause, now? }) | check(), openFor(), trip(seconds?), reset(). | | checkBinding(binding, key, { period?, limit?, now? }) | A Cloudflare ratelimits binding as a Decision; missing binding → unlimited. | | firstBinding(...bindings) | The first that exists. | | new MemoryStore({ now? }) | In-process counters. | | new D1Store(db, { table?, now? }), d1Schema(table?) | D1 counters; prune() deletes expired rows. | | hashIdentity(identity) | 32 hex characters of its SHA-256. |

A store implements increment(key, ttl, by?) (atomic), get(key), set(key, value, ttl) and forget(key): bring your own (KV, Durable Objects, Redis) by implementing those four.

Notes

  • A refused attempt still counts, so hammering keeps a caller locked out. Use peek() to look without counting, and count only real failures (a wrong password) for limits that should forgive successes.
  • D1 doesn't expire rows: call store.prune() from a cron trigger.
  • MemoryStore is per isolate. On Workers, use D1 for anything that must hold across requests.
  • Times are seconds for limits and windows, Unix seconds for resetsAt; the injectable clock is in milliseconds, like Date.now.
  • Unlike the PHP twin, a check counts before it decides (see Why). A limit of 5 still allows exactly 5.

License

MIT