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

convex-rate-limiter

v0.2.1

Published

Fixed-window rate limiting component for Convex apps

Downloads

921

Readme

convex-rate-limiter

Fixed-window rate limiting for Convex apps.

Protect login endpoints from brute force, enforce AI API quotas, and prevent public API abuse — all within your Convex backend.

Install

npm install convex-rate-limiter

Wire Up

In your app's convex/convex.config.ts:

import { defineApp } from "convex/server";
import rateLimiter from "convex-rate-limiter/convex.config.js";

const app = defineApp();
app.use(rateLimiter);
export default app;

Component Layout

This package follows Convex component root layout:

  • convex.config.ts
  • schema.ts
  • rateLimits.ts (public/internal component functions)
  • utils.ts (validation + window parsing)
  • crons.ts (daily cleanup cron)
  • _generated/ (component server/api/component typings)

Usage

Component functions are accessed via the components namespace that Convex generates when you run npx convex dev in your host app. checkRateLimit and enforceRateLimit are mutations; peek is a query.

import { action, query } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";

enforceRateLimit — throw on limit exceeded

The simplest integration. Throws ConvexError if the rate limit is exceeded.

export const login = action({
  args: { email: v.string(), password: v.string() },
  handler: async (ctx, args) => {
    await ctx.runMutation(components.rateLimiter.rateLimits.enforceRateLimit, {
      key: "login:" + args.email,
      limit: 5,
      window: "15m",
    });
    // proceed with login...
  },
});

Map to HTTP 429:

export const rateLimitedAction = action({
  args: { key: v.string() },
  handler: async (ctx, args) => {
    try {
      await ctx.runMutation(components.rateLimiter.rateLimits.enforceRateLimit, {
        key: args.key, limit: 10, window: "1m",
      });
    } catch (e: any) {
      if (e.data?.code === "RATE_LIMITED") {
        return new Response("Too Many Requests", {
          status: 429,
          headers: { "Retry-After": String(Math.ceil((e.data.resetAt - Date.now()) / 1000)) },
        });
      }
      throw e;
    }
    // proceed with protected logic...
  },
});

checkRateLimit — check and handle manually

export const sendMessage = action({
  args: { userId: v.string(), text: v.string() },
  handler: async (ctx, args) => {
    const result = await ctx.runMutation(components.rateLimiter.rateLimits.checkRateLimit, {
      key: "ai-chat:" + args.userId,
      limit: 20,
      window: "1h",
    });

    if (!result.allowed) {
      throw new Error(`Rate limited. Resets in ${Math.ceil((result.resetAt - Date.now()) / 1000)}s`);
    }
    // result.remaining — slots left in this window
  },
});

peek — read-only status (no side effects)

Safe to call from queries and actions. Use for displaying quota in UI.

export const getQuota = query({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    return await ctx.runQuery(components.rateLimiter.rateLimits.peek, {
      key: "ai-chat:" + args.userId,
      limit: 20,
      window: "1h",
    });
    // { remaining: 14, resetAt: 1712345678000 }
    // { remaining: 20, resetAt: null }  ← no active window yet
  },
});

API Reference

All public component functions use object-style syntax, import builders from ./_generated/server, and include explicit args + returns validators for cross-boundary type safety.

Window values

"1m" | "5m" | "15m" | "1h" | "6h" | "24h" | "7d"

checkRateLimit(ctx, { key, limit, window })

| Field | Type | Description | |---|---|---| | allowed | boolean | Whether the request is permitted | | remaining | number | Slots left (limit - count if allowed; 0 if denied) | | resetAt | number | Epoch ms when current window resets |

enforceRateLimit(ctx, { key, limit, window })

Returns { remaining, resetAt } if allowed. Throws ConvexError({ code: "RATE_LIMITED", remaining: 0, resetAt }) if denied.

peek(ctx, { key, limit, window })

| Field | Type | Description | |---|---|---| | remaining | number | Slots left in current window | | resetAt | number \| null | null if no active window |

How It Works

Uses a fixed-window algorithm: each key tracks a request count and the timestamp when the current window opened. Expired windows reset on the next access. Convex's per-document mutation serialization guarantees correctness under concurrent requests — no locks needed.

A daily background job removes stale records (windows older than 8 days).

License

MIT