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.
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:
Filter and sort indexes: Stored in Postgres, these allow you to filter and sort records in your API queries
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:
Open the AI assistant in your Gadget editor
Ask it to "optimize indexes" or "remove unused indexes"
Review the AI's recommendations
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.
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.
TypeScript
// 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 webhook payload data before issuing additional reads
When an action is triggered by a webhook, check the contents of trigger.payload first for things like related records before reading from Shopify or your own database again. This reduces backend request, DB read, and Shopify field fetch platform credits, plus CPU time.
api/models/shopifyOrder/actions/update.ts
TypeScript
import { save } from "gadget-server";
export const run: ActionRun = async ({ record, trigger }) => {
if (trigger.type !== "shopify_webhook") return;
// Use related data already present on the webhook payload
// This avoids an extra API call to fetch order line items
const lineItems = trigger.payload?.line_items ?? [];
const hasGiftCardLineItem = lineItems.some((item) => item.gift_card === true);
record.hasGiftCard = hasGiftCardLineItem;
await save(record);
};
import { save } from "gadget-server";
export const run: ActionRun = async ({ record, trigger }) => {
if (trigger.type !== "shopify_webhook") return;
// Use related data already present on the webhook payload
// This avoids an extra API call to fetch order line items
const lineItems = trigger.payload?.line_items ?? [];
const hasGiftCardLineItem = lineItems.some((item) => item.gift_card === true);
record.hasGiftCard = hasGiftCardLineItem;
await save(record);
};
Use record.changed to avoid unnecessary writes and loops
Use record.changed("fieldName") to skip work when the relevant field did not change. This is especially important in webhook-driven integrations where writing back to an external API can trigger another webhook.
api/models/shopifyProduct/actions/update.ts
TypeScript
export const run: ActionRun = async ({ record, logger }) => {
if (!record.changed("title")) return;
logger.info("title changed, running expensive sync step");
// run external sync logic only when title changed
};
export const run: ActionRun = async ({ record, logger }) => {
if (!record.changed("title")) return;
logger.info("title changed, running expensive sync step");
// run external sync logic only when title changed
};
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
TypeScript
// 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:
TypeScript
// 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.
If you need to iterate through all records, use iterateAllFramework v1.5.0+ instead of writing a manual pagination loop. It automatically fetches pages of 250 records and yields them one at a time:
TypeScript
for await (const product of api.product.iterateAll()) {
// process each record
}
for await (const product of api.product.iterateAll()) {
// process each record
}
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:
Review all places where you call api.enqueue() to run background actions
Ask yourself: Does this work need to happen asynchronously and/or durably, or could it run in the foreground?
Consider whether multiple background actions could be combined into a single action
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
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:
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.
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 actions, 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
TypeScript
// 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);
You can also use the internal API to run bulk operations that do not need action code to be run. Note that using the internal API skips tenancy and access control checks.
TypeScript
// VERY EFFICIENT: One bulk call for all records, skipping action code
const payload = importedItems.map((item) => ({
title: item.title,
price: item.price,
}));
const records = await api.internal.product.bulkCreate(payload);
// VERY EFFICIENT: One bulk call for all records, skipping action code
const payload = importedItems.map((item) => ({
title: item.title,
price: item.price,
}));
const records = await api.internal.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.
Review which Shopify models your app actually uses in actions, routes, and frontend code
Navigate to Settings > Plugins > Shopify in the Gadget editor
Click Edit to see the models currently selected
Disable any models you do not need by unchecking them
Save your changes
Then remove the model from your Gadget app:
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:
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.
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.
Configure non-webhook fields and related models to fetch later Framework v1.6.0+
Some Shopify fields are not included in webhook payloads and must be fetched separately from the Shopify API. By default, Gadget can fetch these fields immediately when processing a webhook, but each fetch consumes your Shopify API rate limit and increases Gadget request time.
For fields you do not need immediately, configure them to fetch later instead of fetch on webhook. Fields set to fetch later are populated during nightly reconciliation or manual syncs, avoiding the per-webhook API cost.
To configure non-webhook fields:
Open the Shopify model in the Gadget editor
Select a non-webhook field
Choose fetch later instead of fetch on webhook
This reduces:
Platform credits: Shopify field fetch operations are not triggered on each webhook
Shopify API usage: Fewer requests count against your rate limit
Resource charges: Less request time spent waiting for Shopify API responses
For models with many non-webhook fields or high webhook volume, this optimization can significantly reduce costs. See non-webhook fields and models for more details on configuring fetch behavior.
Delete stale records you no longer need
If your app does not require full historical data, schedule cleanup for stale records to reduce data storage and search storage resource charges. This can also reduce DB read and DB write compute for future queries and syncs.
For Shopify apps, do not remove the shopifyUpdatedAt field from Shopify models. Gadget uses it for reconciliation and sync behavior.
Optimize your BigCommerce app
Many of the same patterns apply to optimizing BigCommerce apps as they do for Shopify apps.
Store only required BigCommerce fields
Do not mirror BigCommerce resources field-for-field unless your app needs all of them. Storing only required fields reduces data storage and search storage resource charges, and lowers DB read and DB write compute for every query and write.
Process BigCommerce webhooks without storing everything
If you only need to react to a BigCommerce webhook, process it in a global action and persist only the minimum data required for your feature. This reduces DB write platform credits and storage growth.
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.
Simple CRUD operations: Use Internal API for straightforward creates and updates without running action code
Bulk deletions by filter: Use api.internal.*.deleteMany() instead of running delete actions for each record.
TypeScript
// LESS EFFICIENT: Runs action code for each record
for await (const product of api.product.iterateAll({
filter: { status: { equals: "inactive" } },
select: { id: true },
})) {
await api.product.delete(product.id);
}
// MORE EFFICIENT: Uses Internal API to delete without running action code
await api.internal.product.deleteMany({
filter: { status: { equals: "inactive" } },
});
// LESS EFFICIENT: Runs action code for each record
for await (const product of api.product.iterateAll({
filter: { status: { equals: "inactive" } },
select: { id: true },
})) {
await api.product.delete(product.id);
}
// MORE EFFICIENT: Uses Internal API to delete without running action code
await api.internal.product.deleteMany({
filter: { status: { equals: "inactive" } },
});
Only use api.internal.*.deleteMany() when delete action code is not required. If you need to run delete actions, use
api.*.bulkDelete().
Prefer mutating record inside model actions
When you are already inside a model action for a specific record, update that record directly and save it, instead of making another API call. This avoids an extra backend request and additional action overhead.
api/models/product/actions/update.ts
TypeScript
import { save } from "gadget-server";
export const run: ActionRun = async ({ record }) => {
record.lastSyncedAt = new Date();
await save(record);
};
import { save } from "gadget-server";
export const run: ActionRun = async ({ record }) => {
record.lastSyncedAt = new Date();
await save(record);
};
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:
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.ts
TypeScript
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.
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.
import { Server } from "gadget-server";
import rateLimit from "@fastify/rate-limit";
/**
* Route plugin for *
*
* @see https://www.fastify.dev/docs/latest/Reference/Server
*/
export default async function (server: Server) {
await server.register(rateLimit, {
max: 100,
timeWindow: "1 minute",
});
}
import { Server } from "gadget-server";
import rateLimit from "@fastify/rate-limit";
/**
* Route plugin for *
*
* @see https://www.fastify.dev/docs/latest/Reference/Server
*/
export default async function (server: Server) {
await server.register(rateLimit, {
max: 100,
timeWindow: "1 minute",
});
}
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 edge 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:
Review your dependencies
Use yarn remove to remove unused packages
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.
Avoid eager API calls on every page
Only call APIs when a page needs the data. For example, if billing plan data is only shown on a billing page, do not fetch it in your root loader. This reduces edge request and backend request platform credits, plus CPU time from unnecessary loaders.
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
If files expire after a known retention window, schedule cleanup: