Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Audit and fix Convex performance issues: hot reads, OCC conflicts, subscription cost, and function limits.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/function-budget.md
1# Function Budget23Use these rules when functions are hitting execution limits, transaction size4errors, or returning excessively large payloads to the client.56## Core Principle78Convex functions run inside transactions with budgets for time, reads, and9writes. Staying well within these limits is not just about avoiding errors, it10reduces latency and contention.1112## Limits to Know1314These are the current values from the15[Convex limits docs](https://docs.convex.dev/production/state/limits). Check16that page for the latest numbers.1718| Resource | Limit |19| --------------------------------- | ----------------------------------------------------- |20| Query/mutation execution time | 1 second (user code only, excludes DB operations) |21| Action execution time | 10 minutes |22| Data read per transaction | 16 MiB |23| Data written per transaction | 16 MiB |24| Documents scanned per transaction | 32,000 (includes documents filtered out by `.filter`) |25| Index ranges read per transaction | 4,096 (each `db.get` and `db.query` call) |26| Documents written per transaction | 16,000 |27| Individual document size | 1 MiB |28| Function return value size | 16 MiB |2930## Symptoms3132- "Function execution took too long" errors33- "Transaction too large" or read/write set size errors34- Slow queries that read many documents35- Client receiving large payloads that slow down page load36- `npx convex insights --details` showing high bytes read3738## Common Causes3940### Unbounded collection4142A query that calls `.collect()` on a table without a reasonable limit. As the43table grows, the query reads more and more documents.4445### Large document reads on hot paths4647Reading documents with large fields (rich text, embedded media references, long48arrays) when only a small subset of the data is needed for the current view.4950### Mutation doing too much work5152A single mutation that updates hundreds of documents, backfills data, or53rebuilds derived state in one transaction.5455### Returning too much data to the client5657A query returning full documents when the client only needs a few fields.5859## Fix Order6061### 1. Bound your reads6263Never `.collect()` without a limit on a table that can grow unbounded.6465```ts66// Bad: unbounded read, breaks as the table grows67const messages = await ctx.db.query("messages").collect();68```6970```ts71// Good: paginate or limit72const messages = await ctx.db73.query("messages")74.withIndex("by_channel", (q) => q.eq("channelId", channelId))75.order("desc")76.take(50);77```7879### 2. Read smaller shapes8081If the list page only needs title, author, and date, do not read full documents82with rich content fields.8384Use digest or summary tables for hot list pages. See `hot-path-rules.md` for the85digest table pattern.8687### 3. Break large mutations into batches8889If a mutation needs to update hundreds of documents, split it into a90self-scheduling chain.9192```ts93// Bad: one mutation updating every row94export const backfillAll = internalMutation({95handler: async (ctx) => {96const docs = await ctx.db.query("items").collect();97for (const doc of docs) {98await ctx.db.patch(doc._id, { newField: computeValue(doc) });99}100},101});102```103104```ts105// Good: cursor-based batch processing106export const backfillBatch = internalMutation({107args: { cursor: v.optional(v.string()), batchSize: v.optional(v.number()) },108handler: async (ctx, args) => {109const batchSize = args.batchSize ?? 100;110const result = await ctx.db111.query("items")112.paginate({ cursor: args.cursor ?? null, numItems: batchSize });113114for (const doc of result.page) {115if (doc.newField === undefined) {116await ctx.db.patch(doc._id, { newField: computeValue(doc) });117}118}119120if (!result.isDone) {121await ctx.scheduler.runAfter(0, internal.items.backfillBatch, {122cursor: result.continueCursor,123batchSize,124});125}126},127});128```129130### 4. Move heavy work to actions131132Queries and mutations run inside Convex's transactional runtime with strict133budgets. If you need to do CPU-intensive computation, call external APIs, or134process large files, use an action instead.135136Actions run outside the transaction and can call mutations to write results137back.138139```ts140// Bad: heavy computation inside a mutation141export const processUpload = mutation({142handler: async (ctx, args) => {143const result = expensiveComputation(args.data);144await ctx.db.insert("results", result);145},146});147```148149```ts150// Good: action for heavy work, mutation for the write151export const processUpload = action({152handler: async (ctx, args) => {153const result = expensiveComputation(args.data);154await ctx.runMutation(internal.results.store, { result });155},156});157```158159### 5. Trim return values160161Only return what the client needs. If a query fetches full documents but the162component only renders a few fields, map the results before returning.163164```ts165// Bad: returns full documents including large content fields166export const list = query({167handler: async (ctx) => {168return await ctx.db.query("articles").take(20);169},170});171```172173```ts174// Good: project to only the fields the client needs175export const list = query({176handler: async (ctx) => {177const articles = await ctx.db.query("articles").take(20);178return articles.map((a) => ({179_id: a._id,180title: a.title,181author: a.author,182createdAt: a._creationTime,183}));184},185});186```187188### 6. Replace `ctx.runQuery` and `ctx.runMutation` with helper functions189190Inside queries and mutations, `ctx.runQuery` and `ctx.runMutation` have overhead191compared to calling a plain TypeScript helper function. They run in the same192transaction but pay extra per-call cost.193194```ts195// Bad: unnecessary overhead from ctx.runQuery inside a mutation196export const createProject = mutation({197handler: async (ctx, args) => {198const user = await ctx.runQuery(api.users.getCurrentUser);199await ctx.db.insert("projects", { ...args, ownerId: user._id });200},201});202```203204```ts205// Good: plain helper function, no extra overhead206export const createProject = mutation({207handler: async (ctx, args) => {208const user = await getCurrentUser(ctx);209await ctx.db.insert("projects", { ...args, ownerId: user._id });210},211});212```213214Exception: components require `ctx.runQuery`/`ctx.runMutation`. Use them there,215but prefer helpers everywhere else.216217### 7. Avoid unnecessary `runAction` calls218219`runAction` from within an action creates a separate function invocation with220its own memory and CPU budget. The parent action just sits idle waiting. Replace221with a plain TypeScript function call unless you need a different runtime (e.g.222calling Node.js code from the Convex runtime).223224```ts225// Bad: runAction overhead for no reason226export const processItems = action({227handler: async (ctx, args) => {228for (const item of args.items) {229await ctx.runAction(internal.items.processOne, { item });230}231},232});233```234235```ts236// Good: plain function call237export const processItems = action({238handler: async (ctx, args) => {239for (const item of args.items) {240await processOneItem(ctx, { item });241}242},243});244```245246## Verification2472481. No function execution or transaction size errors2492. `npx convex insights --details` shows reduced bytes read2503. Large mutations are batched and self-scheduling2514. Client payloads are reasonably sized for the UI they serve2525. `ctx.runQuery`/`ctx.runMutation` in queries and mutations replaced with253helpers where possible2546. Sibling functions with similar patterns were checked255