Optimizing your Gadget bill 

This guide covers strategies for reducing your Gadget resource usage and costs. By following these optimization techniques, you can ensure you are only paying for the resources your app actually needs.

Remove unused indexes  

One of the most effective ways to reduce your Gadget bill is to remove indexes you are not using. Indexes enable your application to search, filter, and sort data, but they consume storage and require compute resources to maintain. If you are not filtering, sorting, or searching on certain fields, you can safely disable their indexes.

Gadget maintains two types of indexes:

  1. Filter and sort indexes: Stored in Postgres, these allow you to filter and sort records in your API queries
  2. Search indexes: Stored in Elasticsearch, these enable full-text search across your data

Removing unused indexes reduces three billing line items:

  • Search storage resource charges for Elasticsearch indexes
  • Data storage resource charges for Postgres indexes
  • Platform credits for search write operations when records are created or updated

Note that disabling indexes requires database writes. These writes will be a one-time cost unless the index is re-enabled later, and will be reflected in your operations dashboard.

Using the AI assistant to optimize indexes 

The easiest way to optimize your indexes is to use Gadget's AI assistant. The assistant can analyze your application's field usage and automatically identify indexes that can be safely removed.

To optimize indexes using the AI assistant:

  1. Open the AI assistant in your Gadget editor
  2. Ask it to "optimize indexes" or "remove unused indexes"
  3. Review the AI's recommendations
  4. Accept the changes to remove unused indexes from your models

The AI assistant will analyze your models and identify fields that have indexing enabled but are never used for filtering, sorting, or searching in your application code.

Indexes can also be managed manually in the Gadget editor. See field indexing configuration for more details.

Optimize database reads, writes, and background work 

Database operations and background actions drive several billing line items: DB read and DB write platform credits, DB read and write compute resource charges, and background enqueue and action run platform credits.

The strategies below help you reduce these costs by fetching only what you need, pushing work into the database where possible, and using background actions only when necessary.

Use field selection in queries 

By default, Gadget fetches all fields from your models. If you only need specific fields, use the select parameter to fetch only what you need. This reduces the DB read compute resource charge by minimizing the amount of data transferred from the database.

JavaScript
// INEFFICIENT: Fetches all fields const products = await api.product.findMany(); // EFFICIENT: Only fetches the fields you need const products = await api.product.findMany({ select: { id: true, title: true, price: true, }, });
// INEFFICIENT: Fetches all fields const products = await api.product.findMany(); // EFFICIENT: Only fetches the fields you need const products = await api.product.findMany({ select: { id: true, title: true, price: true, }, });

Use computed views instead of pagination 

If you need to aggregate or process data across many records, avoid fetching all records with pagination. Paginating through records increases three billing line items: backend request platform credits, DB read platform credits, and the CPU time resource charge to process the data in JavaScript. Instead, use computed views to run aggregations directly in the database.

Why computed views are more efficient 

Paginating over all records to compute aggregates increases these billing line items:

  • Platform credits: backend request operations, one per page
  • Platform credits: DB read operations, one per page
  • Resource charges: DB read compute based on data transferred from the database
  • Resource charges: CPU time to process data in JavaScript

Computed views run aggregations in Postgres, which reduces costs:

  • Platform credits: single backend request operation instead of many
  • Platform credits: single DB read operation instead of many
  • Resource charges: minimal DB read compute because only the result is transferred, not all the raw data
  • Resource charges: minimal CPU time because the database does the aggregation, not JavaScript

Example: Computing totals 

JavaScript
// INEFFICIENT: Fetching all records and computing in JavaScript let total = 0; let hasNextPage = true; let cursor = null; while (hasNextPage) { const result = await api.order.findMany({ first: 250, after: cursor, }); for (const order of result) { total += order.amount; } cursor = result.endCursor; hasNextPage = result.hasNextPage; } // EFFICIENT: Use a computed view with aggregation const result = await api.orderTotals(); const total = result.total;
// INEFFICIENT: Fetching all records and computing in JavaScript let total = 0; let hasNextPage = true; let cursor = null; while (hasNextPage) { const result = await api.order.findMany({ first: 250, after: cursor, }); for (const order of result) { total += order.amount; } cursor = result.endCursor; hasNextPage = result.hasNextPage; } // EFFICIENT: Use a computed view with aggregation const result = await api.orderTotals(); const total = result.total;

For more information on creating and using computed views, see the computed views guide.

Adjust pagination limits when needed 

If you do need to paginate, you can adjust the pagination limit to fetch more records at once. The default pagination limit in Gadget is 50 records. If you need to process more records at once, you can fetch up to 250 records per page:

JavaScript
// Default: fetches 50 records const products = await api.product.findMany(); // Fetch more records per page (up to 250) const products = await api.product.findMany({ first: 250, });
// Default: fetches 50 records const products = await api.product.findMany(); // Fetch more records per page (up to 250) const products = await api.product.findMany({ first: 250, });

Fetching more records per page reduces the number of backend request platform credits consumed. For example, fetching 1,000 records requires 20 backend requests at the default 50 per page, but only 4 backend requests at 250 per page.

Minimize background actions 

Background actions are powerful for offloading work from foreground requests or enforcing request durability, but each one increases two billing line items: background enqueue request platform credits when you call api.enqueue() and background action run platform credits when the action executes. They also consume the CPU time resource charge while executing. Review your background action usage to ensure each one is necessary.

To optimize background action usage:

  1. Review all places where you call api.enqueue() to run background actions
  2. Ask yourself: Does this work need to happen asynchronously and/or durably, or could it run in the foreground?
  3. Consider whether multiple background actions could be combined into a single action
  4. Use high priority only for truly urgent background work to avoid unnecessary surge compute charges

Background actions are best for:

  • Long-running operations that would timeout in a foreground request
  • Work that can be deferred and does not need immediate results
  • Operations that should not block user-facing requests
  • Operations that must be retried if they fail

For more information, see the background actions guide.

Limit the number of retries 

Background actions are retried up to 6 times by default, in production environments. This is useful for transient errors that are likely to resolve over time, like rate limiting errors.

If most of your background action retries are due to non-transient errors, you can limit the number of retries to avoid excessive platform credits and resource charges:

only 2 retries allowed
JavaScript
await api.enqueue(api.someAction, { ...params }, { retries: 2 });
await api.enqueue(api.someAction, { ...params }, { retries: 2 });

Handle external API rate limits efficiently 

Rate limits on external APIs can trigger repeated retries and increase platform credits and resource charges. You can reduce this by limiting concurrency, using exponential backoff, or honoring rate limit headers.

Use concurrency control to prevent 429s 

Limit how many actions call the API at once by using a queue with maxConcurrency that matches the API's rate limit. See queuing and concurrency control.

api/models/widget/actions/callExternalApi.js
JavaScript
await api.enqueue(api.callToExternalApi, params, { queue: { name: "external-api-queue", maxConcurrency: 2 }, });
await api.enqueue(api.callToExternalApi, params, { queue: { name: "external-api-queue", maxConcurrency: 2 }, });
Use exponential backoff for retries 

Configure retries with backoff so failed calls are spaced out instead of retrying immediately:

api/models/widget/actions/callExternalApi.js
JavaScript
await api.enqueue(api.callToExternalApi, params, { retries: { retryCount: 5, initialInterval: 5000, backoffFactor: 2, maxInterval: 120000, }, });
await api.enqueue(api.callToExternalApi, params, { retries: { retryCount: 5, initialInterval: 5000, backoffFactor: 2, maxInterval: 120000, }, });
Use Retry-After or X-RateLimit-Remaining headers 

On 429 responses, use the API's Retry-After (or similar) header to schedule a single retry instead of relying on multiple backoff retries:

api/models/widget/actions/callExternalApi.js
JavaScript
if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After") || "60"); await api.enqueue(api.callToExternalApi, params, { startAt: new Date(Date.now() + retryAfter * 1000).toISOString(), }); return { rateLimited: true }; }
if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After") || "60"); await api.enqueue(api.callToExternalApi, params, { startAt: new Date(Date.now() + retryAfter * 1000).toISOString(), }); return { rateLimited: true }; }

Use bulk operations instead of looping 

Do not run an action in a loop for each record. Each action call increases backend request platform credits and the CPU time resource charge. Use bulk operations instead: bulkCreate, bulkUpdate, and bulkDelete process many records in a single API call and reduce these costs.

Bulk actions are also available on custom model-scoped action, and when using bulk enqueuing.

Why looping is inefficient 

Calling an action once per record increases:

  • Platform credits: one backend request per record
  • Resource charges: CPU time to run action code and apply access control per record

Bulk operations run as a single request and process records in parallel under the hood, which reduces backend request platform credits and often reduces total CPU time.

Example: Creating records 

JavaScript
// INEFFICIENT: One action call per record for (const item of importedItems) { await api.product.create({ title: item.title, price: item.price, }); } // EFFICIENT: One bulk call for all records const payload = importedItems.map((item) => ({ title: item.title, price: item.price, })); const records = await api.product.bulkCreate(payload);
// INEFFICIENT: One action call per record for (const item of importedItems) { await api.product.create({ title: item.title, price: item.price, }); } // EFFICIENT: One bulk call for all records const payload = importedItems.map((item) => ({ title: item.title, price: item.price, })); const records = await api.product.bulkCreate(payload);

For more information on bulk actions, including custom model-scoped actions and running them in the background, see bulk actions and bulk enqueuing.

Optimize your Shopify app 

If your Gadget app uses the Shopify connection, the strategies below reduce platform credits and resource charges for syncing, webhooks, and storage.

Remove unused Shopify models 

You may have models for Shopify resources that your app does not actually need. Removing them is a two-step process: disable the model in the Shopify connection configuration, then remove the model from your Gadget app.

  1. Review which Shopify models your app actually uses in actions, routes, and frontend code
  2. Navigate to Settings > Plugins > Shopify in the Gadget editor
  3. Click Edit to see the models currently selected
  4. Disable any models you do not need by unchecking them
  5. Save your changes

Then remove the model from your Gadget app:

  1. Right-click on the model in the file tree and Remove.

When you disable a Shopify model, you eliminate ongoing charges for that model:

  • Gadget will no longer sync data for that model from Shopify
  • Platform credits: webhook action run and filter and idempotency check operations will stop for that model
  • Resource charges: data storage and search storage will stop growing; existing data will be deleted

Make sure to check your code for any references to disabled models before removing them. Disabling a model that is still in use will cause errors in your app.

Remove unused Shopify fields 

You can remove unused fields from your Shopify models to reduce data storage, search storage, and platform credits for search and database reads and writes.

Optimize Shopify data syncing 

Shopify data syncs consume platform credits for backend request, DB read, DB write, and search write operations. For stores with large product catalogs or order histories, syncing all historical data can consume significant platform credits. You can reduce costs by syncing only the data you need.

Use model selection 

Do not sync every Shopify model if you do not need to. Instead, specify the models you need to sync when you call the sync API:

JavaScript
await api.shopifySync.run({ domain: shop.domain, shop: { _link: params.shopId, }, models: ["shopifyProduct"], });
await api.shopifySync.run({ domain: shop.domain, shop: { _link: params.shopId, }, models: ["shopifyProduct"], });

See sync by model for more details.

Note: Syncing by date-time and syncing by model can be combined.

Use time range selection 

When syncing historical Shopify data, you can limit the sync to a specific time range instead of syncing all historical data:

JavaScript
const millisecondsPerDay = 1000 * 60 * 60 * 24; const syncSince = new Date(); // sync from 5 days ago syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay); await api.shopifySync.run({ domain: shop.domain, shop: { _link: params.shopId }, syncSince, });
const millisecondsPerDay = 1000 * 60 * 60 * 24; const syncSince = new Date(); // sync from 5 days ago syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay); await api.shopifySync.run({ domain: shop.domain, shop: { _link: params.shopId }, syncSince, });

See sync by date-time for more details.

Note: Syncing by date-time and syncing by model can be combined.

Optimize Shopify webhook processing 

If you are building a Shopify app, webhook processing increases three platform credit line items: webhook action run operations when your action executes, filter and idempotency check operations to determine if processing is needed, and Shopify field fetch operations to retrieve fields not in the webhook payload. Optimize webhook handling to reduce these costs.

Use webhook filters 

Gadget allows you to filter which webhooks trigger your actions. Only process webhooks for records you actually need:

  1. Go to your Shopify model action settings
  2. Configure webhook filters to only process relevant records

This reduces platform credits for webhook action run and filter and idempotency check operations by preventing unnecessary webhook processing.

Use includeFields to reduce fetches 

Use the includeFields configuration to specify exactly which fields are included in the webhook payload.

This reduces platform credits for Shopify field fetch operations by fetching only the fields you need instead of all available fields.

Use global actions and Shopify webhook triggers if you do not need to save data 

If you do not need to store Shopify data, and do not need the built-in reconciliation for missed webhooks, you can use global actions and webhook triggers to process Shopify data.

Shopify webhook triggers on global actions allow you to specify specific webhook topics to listen to and run the global action when they are received.

See Shopify webhooks within global actions for more details.

Use the Internal API to reduce action overhead 

Actions in Gadget run your custom code and apply access control. Each action invocation increases two billing line items: backend request platform credits and the CPU time resource charge. For internal operations where you do not need custom logic or access control, use the Internal API instead to reduce these costs.

When to use the Internal API 

  • Simple CRUD operations: Use Internal API for straightforward creates and updates without running action code
  • Bulk operations: Use internalApi.*.bulkDelete() instead of running delete actions for each record
JavaScript
// LESS EFFICIENT: Runs action code for each record await api.product.bulkDelete({ filter: { status: { equals: "inactive" } }, }); // MORE EFFICIENT: Uses Internal API to delete without running action code await internalApi.product.bulkDelete({ filter: { status: { equals: "inactive" } }, });
// LESS EFFICIENT: Runs action code for each record await api.product.bulkDelete({ filter: { status: { equals: "inactive" } }, }); // MORE EFFICIENT: Uses Internal API to delete without running action code await internalApi.product.bulkDelete({ filter: { status: { equals: "inactive" } }, });

Use helpers instead of global actions 

If you are using global actions just to share code between actions, consider using helper functions instead. Create helper files in your project and import them where needed:

api/models/product/actions/applyDiscount.js
JavaScript
// helpers/shopify.ts - shared helper function export const calculateDiscount = (price: number, percentage: number) => { return price * (1 - percentage / 100); }; import { calculateDiscount } from "../../helpers/shopify"; export const run: ActionRun = async ({ params, record }) => { record.discountedPrice = calculateDiscount(record.price, params.discount); await save(record); };
// helpers/shopify.ts - shared helper function export const calculateDiscount = (price: number, percentage: number) => { return price * (1 - percentage / 100); }; import { calculateDiscount } from "../../helpers/shopify"; export const run: ActionRun = async ({ params, record }) => { record.discountedPrice = calculateDiscount(record.price, params.discount); await save(record); };

This avoids consuming backend request platform credits and the CPU time resource charge when you just need shared logic without action lifecycle or access control.

Do not create a lib folder inside actions, models, or routes directories. Create helper files at the project root level and import them where needed.

Optimize HTTP routes 

HTTP routes increase three billing line items: backend request platform credits, edge request platform credits, and the CPU time resource charge to execute your route code. Optimize your routes to reduce these costs.

Enable response caching 

For routes that serve data that does not change frequently, enable response caching to serve cached responses from the edge:

api/routes/GET-products.js
JavaScript
import type { RouteHandler } from "gadget-server"; export const route: RouteHandler = async ({ reply }) => { // Set cache headers reply.header("Cache-Control", "public, max-age=3600"); // Cache for 1 hour reply.send({ data: "your response" }); };
import type { RouteHandler } from "gadget-server"; export const route: RouteHandler = async ({ reply }) => { // Set cache headers reply.header("Cache-Control", "public, max-age=3600"); // Cache for 1 hour reply.send({ data: "your response" }); };

Cached responses are served from the edge network without hitting your backend, eliminating backend request platform credits and the CPU time resource charge for cached responses. Only the edge request platform credit is consumed.

Use ETag headers for conditional requests 

For routes that serve files or large data payloads, use ETag and If-None-Match headers to enable conditional requests. Include the ETag header in your response when serving a resource. On subsequent requests, clients send the If-None-Match header with the previously received ETag value. If the resource has not changed, respond with a 304 Not Modified status instead of downloading or processing the resource again.

This reduces file download platform credits and backend request platform credits by avoiding unnecessary downloads and processing when resources have not changed.

api/routes/GET-image-[imageId].js
JavaScript
import type { RouteHandler } from "gadget-server"; export const route: RouteHandler<{ Params: { imageId: string } }> = async ({ request, reply, }) => { const image = await api.file.maybeFindOne(request.params.imageId, { select: { file: { url: true, }, fileUpdatedAt: true, }, }); if (!image) { return reply.code(404).send({ error: "Image not found" }); } const etag = `"${file.fileUpdatedAt.getTime()}"`; const ifNoneMatch = request.headers["if-none-match"]; if (ifNoneMatch === etag) { // no body needed = cheaper response! return reply.code(304).send(); } return reply .header("ETag", etag) .header("Cache-Control", "public, max-age=3600") .send(image.file.url); };
import type { RouteHandler } from "gadget-server"; export const route: RouteHandler<{ Params: { imageId: string } }> = async ({ request, reply, }) => { const image = await api.file.maybeFindOne(request.params.imageId, { select: { file: { url: true, }, fileUpdatedAt: true, }, }); if (!image) { return reply.code(404).send({ error: "Image not found" }); } const etag = `"${file.fileUpdatedAt.getTime()}"`; const ifNoneMatch = request.headers["if-none-match"]; if (ifNoneMatch === etag) { // no body needed = cheaper response! return reply.code(304).send(); } return reply .header("ETag", etag) .header("Cache-Control", "public, max-age=3600") .send(image.file.url); };

This reduces file download platform credits and backend request platform credits by serving cached responses when resources have not changed, avoiding the cost of downloading or processing the same resource multiple times.

Configure rate limits 

Rate limits help prevent abuse and unexpected cost spikes. Configure appropriate rate limits for your routes:

api/routes/GET-products.js
JavaScript
export const options = { rateLimit: { max: 100, // Maximum 100 requests timeWindow: "1 minute", // Per 1 minute window }, };
export const options = { rateLimit: { max: 100, // Maximum 100 requests timeWindow: "1 minute", // Per 1 minute window }, };

This protects your app from excessive usage that could drive up costs.

Optimize frontend bundle size 

Large frontend bundles increase two billing line items: the edge bandwidth resource charge based on GB of data transferred as users download your JavaScript, CSS, and assets, and frontend request platform credits. Reduce bundle size to lower these costs and improve performance.

Remove unused npm packages 

Regularly audit your package.json and remove packages you are not using:

  1. Review your dependencies
  2. Use yarn remove <package> to remove unused packages
  3. Test your app to ensure everything still works

Using a bundle analyzer can help you identify unused packages and reduce bundle size.

Use server-side rendering strategically 

Instead of loading everything client-side, use server-side rendering to send rendered HTML for initial page loads. This reduces the JavaScript bundle size users need to download.

Use the CDN for static assets 

Gadget automatically serves your frontend assets from a global CDN, but you can optimize further by:

  • Compressing images before uploading
  • Using modern image formats like WebP or AVIF
  • Lazy loading images and components not needed for initial render

Store files efficiently 

For file uploads, Gadget stores files in GCP Cloud Storage. Files increase three billing line items: the file storage resource charge based on GB stored, file upload platform credits when files are uploaded, and file download platform credits when files are accessed. To optimize these costs:

  • Only store files you need; delete old or unused files to reduce file storage resource charges
  • Compress files before uploading to reduce file storage charges and edge bandwidth when serving files
  • Use appropriate file formats, such as WebP for images instead of PNG, to minimize file size

Use profiling to find bottlenecks 

The ggt CLI tool includes a debugger that can profile your app's performance and help identify slow functions and expensive third-party API calls.

See the debugging and profiling guide for examples of how to use the profiler, what to look for, and how to take action.

Was this page helpful?