convex-scale-optimization

Patterns for scaling read-heavy Convex apps to millions of users. Use when optimizing bandwidth, reducing query costs, fixing slow queries, creating digest tables, replacing reactive subscriptions with one-shot fetches, adding compound indexes, debouncing writes, rate-controlling backfills, or running npx convex insights. Trigger when users mention "scale", "bandwidth", "performance", "optimize", "slow queries", "expensive queries", "digest table", "denormalize", or "thundering herd" in the context of Convex.

6 stars

Best use case

convex-scale-optimization is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Patterns for scaling read-heavy Convex apps to millions of users. Use when optimizing bandwidth, reducing query costs, fixing slow queries, creating digest tables, replacing reactive subscriptions with one-shot fetches, adding compound indexes, debouncing writes, rate-controlling backfills, or running npx convex insights. Trigger when users mention "scale", "bandwidth", "performance", "optimize", "slow queries", "expensive queries", "digest table", "denormalize", or "thundering herd" in the context of Convex.

Teams using convex-scale-optimization should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/convex-scale-optimization/SKILL.md --create-dirs "https://raw.githubusercontent.com/get-convex/components-submissions-directory/main/.cursor/skills/convex-scale-optimization/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/convex-scale-optimization/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How convex-scale-optimization Compares

Feature / Agentconvex-scale-optimizationStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Patterns for scaling read-heavy Convex apps to millions of users. Use when optimizing bandwidth, reducing query costs, fixing slow queries, creating digest tables, replacing reactive subscriptions with one-shot fetches, adding compound indexes, debouncing writes, rate-controlling backfills, or running npx convex insights. Trigger when users mention "scale", "bandwidth", "performance", "optimize", "slow queries", "expensive queries", "digest table", "denormalize", or "thundering herd" in the context of Convex.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Scaling Convex Apps

Patterns that took ClawHub from 9TB/day to 600GB/day while serving 1M+ weekly users. Apply these after you have real traffic and confirmed product market fit. Do not use for premature optimization.

Source: https://stack.convex.dev/optimizing-openclaw

## The optimization loop

Run this cycle until the remaining warnings are OCC contention, not bandwidth.

```
1. npx convex insights --prod  → find the top bandwidth consumer
2. Read the function            → understand why it's expensive
3. Fix it                       → usually a data model change
4. npx convex deploy            → live in production, zero downtime
5. Check the dashboard          → is it flat? Go to 1.
```

## Pattern 1: One-shot fetches for public pages

Reactive subscriptions (`useQuery`, `usePaginatedQuery`) re-execute on every write to the read set. For public catalog pages with many readers and frequent background writes, this causes massive amplification.

Replace with `convex.query()` for pages where real-time updates are not needed.

```tsx
// Reactive subscription: re-executes on every write to the read set
const results = usePaginatedQuery(
  api.skills.listPublicPage, args, { initialNumItems: 25 }
)

// One-shot fetch: no subscription, no amplification
const convex = useConvex();
const result = await convex.query(
  api.skills.listPublicPage, { cursor, numItems: 25, sort, dir }
)
```

Manage pagination state in React with `useState` and a generation counter to cancel stale requests.

| Pattern           | Use when                                                                |
|-------------------|-------------------------------------------------------------------------|
| useQuery          | Data is collaboratively edited and every client needs immediate updates |
| usePaginatedQuery | Real-time paginated data with small, bounded page counts                |
| convex.query()    | Many readers, mostly-static data (catalogs, listings, search results)   |

## Pattern 2: Digest tables (denormalization)

Convex returns full documents with no field projections. If your document is 3KB but your listing only needs 200 bytes, you read 15x more than necessary. Joins inside loops compound this.

```tsx
// Expensive: three tables, ~195KB per page of 25 items
for (const skill of skills) {
  const version = await ctx.db.get(skill.latestVersionId)  // 6KB each
  const owner = await ctx.db.get(skill.ownerUserId)         // 1KB each
}
```

Create a lightweight digest table with only the fields your hot path needs, including denormalized fields from joined tables.

```ts
// convex/schema.ts
skillSearchDigest: defineTable({
  skillId: v.id("skills"),
  slug: v.string(),
  displayName: v.string(),
  summary: v.optional(v.string()),
  statsDownloads: v.number(),
  ownerHandle: v.optional(v.string()),     // from users table
  ownerImage: v.optional(v.string()),      // from users table
  latestVersionSummary: v.optional(v.object({ /* minimal fields */ })),
}).index("by_downloads", ["statsDownloads"])
```

Query reads one table, no joins:

```ts
const page = await ctx.db
  .query("skillSearchDigest")
  .withIndex("by_downloads")
  .order("desc")
  .take(25)
```

Result: 195KB per page down to 20KB. A 10x reduction with no UI change.

## Pattern 3: Compare before writing

Sync digest tables using Triggers from `convex-helpers`. But always compare before writing. This was the single highest-impact fix on ClawHub.

```ts
import { Triggers } from "convex-helpers/server/triggers";

const triggers = new Triggers<DataModel>();

triggers.register("skills", async (ctx, change) => {
  if (change.newDoc) {
    await upsertSkillSearchDigest(ctx, change.newDoc);
  } else {
    await deleteSkillSearchDigest(ctx, change.oldDoc._id);
  }
});

export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
```

Inside the upsert, always diff first:

```ts
const existing = await ctx.db.get(digestId);
const changed = DIGEST_KEYS.some((key) => existing[key] !== newFields[key]);
if (!changed) return; // no write = no invalidation
```

Without this, a cron updating stats for 500 skills fires 500 trigger writes. Each write invalidates every active subscriber. Each subscriber re-reads its full page. Cost is `500 writes x subscribers x docs_per_subscriber`. With change detection, most no-op updates become zero-cost.

## Pattern 4: Compound indexes over JS filtering

If you filter documents after the query returns them, you read documents just to throw them away.

```ts
// Scans every document, filters in JS
const allSkills = await ctx.db.query("skills")
  .withIndex("by_active_updated", (q) => q.eq("softDeletedAt", undefined));
const skills = allSkills.filter((skill) => !skill.isSuspicious);

// Database skips non-matching docs entirely
const skills = await ctx.db.query("skills")
  .withIndex("by_nonsuspicious_updated", (q) =>
    q.eq("softDeletedAt", undefined).eq("isSuspicious", false)
  )
```

Audit your codebase for `.filter()` and `if (doc.field) continue` inside query loops. Each one is a candidate for a compound index.

## Pattern 5: Rate-control backfills

When backfilling a new digest table, each batch of writes invalidates active subscribers. Spread writes with a delay between batches.

```ts
if (!batch.isDone) {
  await ctx.scheduler.runAfter(
    1000,
    internal.maintenance.backfillDigest,
    { cursor: batch.continueCursor, batchSize: 100, delayMs: 1000 }
  );
}
```

Add a stop flag: check a control document at the top of each batch so you can halt a runaway backfill.

## Pattern 6: Split heavy mutations

Mutations that read more than 8MB hit Convex transaction limits. Split into Action, Query, Mutation.

```ts
// Hits transaction limits
export const computeLeaderboard = internalMutation({
  handler: async (ctx) => {
    const allSkills = await ctx.db.query("skills").collect();
  },
});

// Action orchestrates, query reads, mutation writes
export const computeLeaderboard = internalAction({
  handler: async (ctx) => {
    const data = await ctx.runQuery(internal.skills.readLeaderboardData);
    const results = computeRankings(data);
    await ctx.runMutation(internal.skills.writeLeaderboardResults, { results });
  },
});
```

Note: each mutation in this pattern runs atomically by itself, but the mutations called from an action do not commit together atomically.

## Quick audit checklist

Run through these when optimizing an existing Convex app:

- [ ] Run `npx convex insights --prod` and identify the top bandwidth consumers
- [ ] Replace `useQuery`/`usePaginatedQuery` with `convex.query()` on public catalog pages
- [ ] Audit `ctx.db.get()` calls inside loops. Can the data live in a digest table?
- [ ] Search for `.filter()` after queries. Replace with compound indexes.
- [ ] Check crons and triggers for unconditional writes. Add change detection.
- [ ] Verify backfill jobs have delay between batches and a stop flag
- [ ] Check for mutations reading more than 8MB. Split into action/query/mutation.

## Key definitions

**Read set**: Every document your query touches becomes part of its read set. For reactive queries, a write to any document in the read set re-executes the entire query.

**Denormalization**: Storing a copy of data from one table inside another so the hot read path avoids joins. Tradeoff: keep the copy in sync.

**Thundering herd**: A batch write fires N triggers. Each trigger write invalidates every active subscriber. Each subscriber re-reads its full page. Cost multiplies as `N x subscribers x docs_per_subscriber`.

**Compound index**: An index on multiple fields. The database walks the B-tree to the first match and scans forward, skipping non-matching documents instead of reading and filtering.

## When NOT to apply these patterns

- Fewer than thousands of documents and moderate traffic
- Early stage prototyping before product market fit
- Collaborative features where every client needs immediate real-time updates
- Internal tools with low concurrent user counts

Ship first. Optimize after you have users.

## Further reading

- [Optimizing OpenClaw (source post)](https://stack.convex.dev/optimizing-openclaw)
- [Queries that Scale](https://docs.convex.dev/production/best-practices/)
- [Triggers (convex-helpers)](https://github.com/get-convex/convex-helpers)
- [Custom Functions](https://docs.convex.dev/functions)
- [Convex Insights CLI](https://docs.convex.dev/dashboard)

Related Skills

workos-convex-debug

6
from get-convex/components-submissions-directory

Debug and troubleshoot WorkOS AuthKit authentication issues with Convex. Use when authentication fails, JWT validation errors occur, user identity returns null, email claims are missing, admin access checks fail, or sign in button does not work. Supports Netlify deployment.

workos-convex-auth

6
from get-convex/components-submissions-directory

Set up and configure WorkOS AuthKit authentication with Convex backend. Use when integrating AuthKit, configuring JWT providers, setting up environment variables, or implementing sign in and sign out flows with React and Vite. Supports Netlify deployment.

convex-design-system

6
from get-convex/components-submissions-directory

Convex UI component patterns from the live Storybook preview. Use when building React components, forms, modals, navigation, feedback states, or app layouts that should match the current Convex design system. Applies to both shared primitives and dashboard style product UI.

convex-self-hosting

6
from get-convex/components-submissions-directory

Integrate Convex static self hosting into existing apps using the latest upstream instructions from get-convex/self-hosting every time. Use when setting up upload APIs, HTTP routes, deployment scripts, migration from external hosting, or troubleshooting static deploy issues across React, Vite, Next.js, and other frontends.

convex-return-validators

6
from get-convex/components-submissions-directory

Guide for when to use and when not to use return validators in Convex functions. Use this skill whenever the user is writing Convex queries, mutations, or actions and needs guidance on return value validation. Also trigger when the user asks about Convex type safety, runtime validation, AI-generated Convex code, Convex AI rules, Convex security best practices, or when they're debugging return type issues in Convex functions. Trigger this skill when users mention "validators", "returns", "return type", or "exact types" in the context of Convex development. Also trigger when writing or reviewing Convex AI rules or prompts that instruct LLMs how to write Convex code.

convex-doctor

6
from get-convex/components-submissions-directory

Static analysis checklist for Convex backends covering 72 rules across security, performance, correctness, schema, architecture, configuration, and client-side patterns. Use when writing, reviewing, or auditing Convex code. Trigger on mentions of "convex-doctor", "health score", "static analysis", "anti-patterns", "audit convex", or before shipping backend changes.

convex

6
from get-convex/components-submissions-directory

Routes general Convex requests to the right project skill. Use when the user asks which Convex skill to use or gives an underspecified Convex app task.

convex-setup-auth

6
from get-convex/components-submissions-directory

Sets up Convex auth, identity mapping, and access control. Use for login, auth providers, users tables, protected functions, or roles in a Convex app.

convex-quickstart

6
from get-convex/components-submissions-directory

Creates or adds Convex to an app. Use for new Convex projects, npm create convex@latest, frontend setup, env vars, or the first npx convex dev run.

convex-performance-audit

6
from get-convex/components-submissions-directory

Audits Convex performance for reads, subscriptions, write contention, and function limits. Use for slow features, insights findings, OCC conflicts, or read amplification.

convex-migration-helper

6
from get-convex/components-submissions-directory

Plans Convex schema and data migrations with widen-migrate-narrow and @convex-dev/migrations. Use for breaking schema changes, backfills, table reshaping, or zero-downtime rollouts.

convex-create-component

6
from get-convex/components-submissions-directory

Builds reusable Convex components with isolated tables and app-facing APIs. Use for new components, reusable backend modules, integrations, or component boundary work.