RealtimeKeep your app up to date
AuthenticationOver 80+ OAuth integrations
Convex Components
ComponentsIndependent, modular, TypeScript building blocks for your backend.
Open sourceSelf host and develop locally
AI CodingGenerate high quality Convex code with AI
Compare
Convex vs. Firebase
Convex vs. Supabase
Convex vs. SQL
DocumentationGet started with your favorite frameworks
SearchSearch across Docs, Stack, and Discord
Convex for StartupsStart and scale your company with Convex
Convex for Open SourceSupport for open source projects
TemplatesUse a recipe to get started quickly
Convex ChampionsAmbassadors that support our thriving community
Convex CommunityShare ideas and ask for help in our community Discord
Stack
Stack

Stack is the Convex developer portal and blog, sharing bright ideas and techniques for building with Convex.

Explore Stack
BlogDocsPricing
GitHub
Log inStart building
Back to Components

Convex Debouncer

ikhrustalev's avatar
ikhrustalev/convex-debouncer
View repo
GitHub logoView package

Category

Backend
Convex Debouncer hero image
npm install @ikhrustalev/convex-debouncer

Convex Debouncer

npm version

A server-side debouncing component for Convex. Debounce expensive operations like LLM calls, metrics computation, or any heavy processing that should only run after a period of inactivity.

Why use this?#

When users rapidly trigger expensive operations (like typing in a chat that triggers AI responses, or updating data that requires recomputation), you often want to:

  1. Wait for a pause before processing (avoid wasted computation)
  2. Guarantee the latest state is processed (don't lose the final update)
  3. Provide immediate feedback in some cases (don't make users wait)

This component provides three debouncing modes to handle these scenarios.

Installation#

npm install @ikhrustalev/convex-debouncer

Create a convex.config.ts file in your app's convex/ folder and install the component:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import debouncer from "@ikhrustalev/convex-debouncer/convex.config.js";

const app = defineApp();
app.use(debouncer);

export default app;

Usage#

import { Debouncer } from "@ikhrustalev/convex-debouncer";
import { components, internal } from "./_generated/api";

// Create a Debouncer instance with default configuration
const debouncer = new Debouncer(components.debouncer, {
  delay: 5000,      // 5 second delay
  mode: "sliding",  // Options: "eager" | "fixed" | "sliding"
});

// In your mutation
export const onPropertyUpdate = mutation({
  args: { propertyId: v.string(), data: v.any() },
  handler: async (ctx, args) => {
    // Schedule a debounced metrics computation
    await debouncer.schedule(
      ctx,
      "property-metrics",        // namespace
      args.propertyId,           // key (unique within namespace)
      internal.metrics.compute,  // function to call
      { propertyId: args.propertyId }  // arguments
    );
  },
});

Debouncing Modes#

Sliding Mode (default)#

Each call resets the timer. The function only executes after the delay passes with no new calls.

Call 1 ──▶ Start 5s timer
           ↓ 2s later
Call 2 ──▶ Reset timer to 5s
           ↓ 5s later (no calls)
           Execute with Call 2's args

Best for: Search-as-you-type, auto-save, real-time validation

Fixed Mode#

Timer stays fixed from the first call. Subsequent calls update the arguments but don't extend the timer.

Call 1 ──▶ Start 5s timer ──────────────┐
           ↓ 2s later                   │
Call 2 ──▶ Update args (timer unchanged)│
           ↓ 3s later                   │
           Execute with Call 2's args ◀─┘

Best for: Batch processing, rate-limited APIs, periodic syncs

Eager Mode#

Execute immediately on first call, then queue a trailing call with the latest arguments if more calls come in.

Call 1 ──▶ Execute immediately + start 5s timer ──┐
           ↓ 1s later                              │
Call 2 ──▶ Queue trailing (update args)           │
           ↓ 2s later                              │
Call 3 ──▶ Queue trailing (update to latest args) │
           ↓ timer ends                            │
           Execute with Call 3's args ◀────────────┘

Best for: Real-time collaboration, AI responses (immediate feedback + final state)

API Reference#

Constructor#

const debouncer = new Debouncer(components.debouncer, {
  delay: 5000,       // Delay in milliseconds
  mode: "sliding",   // "eager" | "fixed" | "sliding"
});

schedule()#

Schedule a debounced function call.

const result = await debouncer.schedule(
  ctx,                           // Mutation context
  "namespace",                   // Logical grouping
  "key",                         // Unique identifier within namespace
  internal.myModule.myFunction,  // Function reference to call
  { arg1: "value" },             // Arguments for the function
  { delay: 3000, mode: "fixed" } // Optional: override defaults
);

// result: { executed: boolean, scheduledFor: number }

status()#

Check the status of a pending debounced call.

const status = await debouncer.status(ctx, "namespace", "key");

// status: null (if no pending call) or:
// {
//   pending: true,
//   scheduledFor: 1234567890,  // Unix timestamp
//   retriggerCount: 3,         // Number of times schedule was called
//   mode: "sliding",
//   hasTrailingCall: false     // (eager mode) whether trailing execution is queued
// }

cancel()#

Cancel a pending debounced call.

const cancelled = await debouncer.cancel(ctx, "namespace", "key");
// cancelled: true if a call was cancelled, false if nothing was pending

Examples#

Debouncing Metrics Computation#

const debouncer = new Debouncer(components.debouncer, {
  delay: 5000,
  mode: "sliding",
});

export const onDataChange = mutation({
  args: { entityId: v.string() },
  handler: async (ctx, args) => {
    // Multiple rapid changes will only trigger one computation
    await debouncer.schedule(
      ctx,
      "metrics",
      args.entityId,
      internal.metrics.recompute,
      { entityId: args.entityId }
    );
  },
});

Eager AI Responses#

const debouncer = new Debouncer(components.debouncer, {
  delay: 10000,
  mode: "eager",  // Respond immediately, then ensure we process final state
});

export const onUserMessage = mutation({
  args: { conversationId: v.string(), message: v.string() },
  handler: async (ctx, args) => {
    const result = await debouncer.schedule(
      ctx,
      "ai-responses",
      args.conversationId,
      internal.ai.generateResponse,
      { conversationId: args.conversationId }
    );

    // result.executed is true for the first message (immediate execution)
    // Subsequent messages will queue a trailing execution
  },
});

Fixed Interval Batch Processing#

const debouncer = new Debouncer(components.debouncer, {
  delay: 30000,   // Process every 30 seconds
  mode: "fixed",  // Timer doesn't extend
});

export const queueItem = mutation({
  args: { batchId: v.string(), itemId: v.string() },
  handler: async (ctx, args) => {
    // First item starts the timer, subsequent items just update
    await debouncer.schedule(
      ctx,
      "batches",
      args.batchId,
      internal.batch.process,
      { batchId: args.batchId }
    );
  },
});

Development#

npm i
npm run dev

Run tests:

npm test

License#

Apache-2.0

Found a bug? Feature request? File it here.

Get your app up and running in minutes
Start building
Convex logo
ProductSyncRealtimeAuthOpen sourceAI codingFAQChefMerchPricing
DevelopersDocsBlogComponentsTemplatesConvex for StartupsConvex for Open SourceChampionsChangelogPodcastLLMs.txt
CompanyAbout usBrandInvestorsBecome a partnerJobsNewsEventsTerms of servicePrivacy policySecurity
SocialTwitterDiscordYouTubeLumaLinkedInGitHub
A Trusted Solution
  • SOC 2 Type II Compliant
  • HIPAA Compliant
  • GDPR Verified
©2026 Convex, Inc.