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.
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:
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 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.
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.
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:
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.
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:
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:
Review your dependencies
Use yarn remove <package> 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.
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.