---
title: Going to Production
---
import { Callout } from 'nextra/components';
# Going to Production
This guide covers key practices to prepare a server built with GraphQL.js for production use.
Concerns include concerns include build optimization, caching, error handling, schema
management, and operational monitoring.
## Optimize your build for production
GraphQL.js includes development-time checks that are useful during local testing. For v16 and
earlier, these checks are enabled by default and should be disabled in production to reduce
overhead. See [Development Mode](./development-mode) for further details about these checks
and how to disable them. Starting in v17, development mode is disabled by default, and must
be explicitly enabled for development environments.
<Callout type="info" emoji="ℹ️">
In v16 and earlier, development mode is enabled by default and must be explicitly disabled
in production. Starting in v17, development mode is disabled by default and may require
explicit enabling. See [Development Mode](./development-mode) for details and instructions.
</Callout>
## Secure your schema
GraphQL gives clients a lot of flexibility, which can be a strength or a liability depending on
how it's used. In production, it's important to control how much of your schema is exposed
and how much work a single query is allowed to do.
Common strategies for securing a schema include:
- Disabling introspection for some users
- Limiting query depth or cost
- Enforcing authentication and authorization
- Applying rate limits
These techniques can help protect your server from accidental misuse or intentional abuse.
### Only allow trusted documents
The most reliable way to protect your GraphQL endpoint from malicious requests
is to only allow operations that you trust — those written by your own
engineers — to be executed.
This technique is not suitable for public APIs that are intended to accept
ad-hoc queries from third parties, but if your GraphQL API is only meant to
power your own websites and apps then it is a simple yet incredibly effective
technique to protect your API endpoint.
Implementing the trusted documents pattern is straightforward:
- When deploying a website or application, SHA256 hash the GraphQL documents
(queries, mutations, subscriptions and associated fragments) it contains, and
place them in a trusted store the server has access to.
- When issuing a request from the client, omit the document (`"query":"{...}"`) and
instead provide the document hash (`"documentId": "sha256:..."`).
- The server should retrieve the document from your trusted store via this hash;
if no document is found or no hash is provided then the request should be
rejected.
This pattern not only improves security significantly by preventing malicious
queries, it has a number of additional benefits:
- Reduces network size since you're sending a hash (~64 bytes) rather than the
entire GraphQL document (which can be tens of kilobytes).
- Makes schema evolution easier because you have a concrete list of all the
fields/types that are in use.
- Makes tracking issues easier because you can tie a hash to the
client/deployment that introduced it.
Be careful not to confuse trusted documents (the key component of which are
trust) with automatic persisted queries (APQ) which are a network optimization
potentially open for anyone to use.
Additional resources:
- [GraphQL over HTTP Appendix
A: Persisted Documents](https://github.com/graphql/graphql-over-http/pull/264)
- [GraphQL Trusted Documents](https://benjie.dev/graphql/trusted-documents) and
[Techniques to Protect Your GraphQL API](https://benjie.dev/talks/techniques-to-protect) at benjie.dev
- [@graphql-codegen/client-preset persisted documents](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#persisted-documents) or [graphql-codegen-persisted-query-ids](https://github.com/valu-digital/graphql-codegen-persisted-query-ids#integrating-with-apollo-client) for Apollo Client
- [Persisted queries in Relay](https://relay.dev/docs/guides/persisted-queries/)
- [Persisted queries in URQL](https://www.npmjs.com/package/@urql/exchange-persisted)
- [Persisted documents in gql.tada](https://gql-tada.0no.co/guides/persisted-documents)
- [persisted queries with `fetch()`](https://github.com/jasonkuhrt/graffle/issues/269)
### Control schema introspection
(Unnecessary if you only allow trusted documents.)
Introspection lets clients query the structure of your schema, including types
and fields. While helpful during development, it may be an unnecessary in
production and disabling it may reduce your API's attack surface.
You can disable introspection in production, or only for unauthenticated users:
```js
import { validate, specifiedRules, NoSchemaIntrospectionCustomRule } from 'graphql';
const validationRules = isPublicRequest
? [...specifiedRules, NoSchemaIntrospectionCustomRule]
: specifiedRules;
```
Note that many developer tools rely on introspection to function properly. Use introspection
control as needed for your tools and implementation.
### Limit query complexity
(Can be a development-only concern if you only allow trusted documents.)
GraphQL allows deeply nested queries, which can be expensive to resolve. You can prevent this
with query depth limits or cost analysis.
The following example shows how to limit query depth:
```js
import depthLimit from 'graphql-depth-limit';
const validationRules = [
depthLimit(10),
...specifiedRules,
];
```
Instead of depth, you can assign each field a cost and reject queries that exceed a total budget.
Tools like [`graphql-cost-analysis`](https://github.com/pa-bru/graphql-cost-analysis) can help.
### Require authentication and authorization
GraphQL doesn't include built-in authentication. Instead, you can attach user data to the request
using middleware, and pass this through to the business logic where
authorization should take place:
```js
// From your business logic
const postRepository = {
getBody({ user, post }) {
if (user?.id && (user.id === post.authorId)) {
return post.body
}
return null
}
}
// Resolver for the `Post.body` field:
function Post_body(source, args, context, info) {
// return the post body only if the user is the post's author
return postRepository.getBody({ user: context.user, post: obj })
}
```
For more details, see the [Authentication and Middleware](./authentication-and-express-middleware/) guide.
### Apply rate limiting
To prevent abuse, you can limit how often clients access specific operations or fields. The
[`graphql-rate-limit`](https://github.com/teamplanes/graphql-rate-limit#field-config) package lets
you define rate limits directly in your schema using custom directives.
For more control, you can also implement your own rate-limiting logic using the request
context, such as limiting by user, client ID, or operation name.
## Improve performance
In production, performance often depends on how efficiently your resolvers fetch and process data.
GraphQL allows flexible queries, which means a single poorly optimized query can result in
excessive database calls or slow response times.
### Use batching with DataLoader
The most common performance issue in GraphQL is the N+1 query problem, where nested resolvers
make repeated calls for related data. `DataLoader` helps avoid this by batching and caching
field-level fetches within a single request.
For more information on this issue and how to resolve it, see
[Solving the N+1 Problem with DataLoader](./n1-dataloader/).
### Apply caching where appropriate
You can apply caching at several levels, depending on your server architecture:
- **Resolver-level caching**: Cache the results of expensive operations for a short duration.
- **HTTP caching**: Use persisted queries and edge caching to avoid re-processing
common queries.
- **Schema caching**: If your schema is static, avoid rebuilding it on every request.
For larger applications, consider request-scoped caching or external systems like Redis to avoid
memory growth and stale data.
## Monitor and debug in production
Observability is key to diagnosing issues and ensuring your GraphQL server is running smoothly
in production. This includes structured logs, runtime metrics, and distributed traces to
follow requests through your system.
### Add structured logging
Use a structured logger to capture events in a machine-readable format. This makes logs easier
to filter and analyze in production systems. Popular options include:
- [`pino`](https://github.com/pinojs/pino): Fast, minimal JSON logger
- [`winston`](https://github.com/winstonjs/winston): More configurable with plugin support
You might log things like:
- Incoming operation names
- Validation or execution errors
- Resolver-level timing
- User IDs or request metadata
Avoid logging sensitive data like passwords or access tokens.
### Collect metrics
Operational metrics help track the health and behavior of your server over time.
You can use tools like [Prometheus](https://prometheus.io) or [OpenTelemetry](https://opentelemetry.io)
to capture query counts, resolver durations, and error rates.
There's no built-in GraphQL.js metrics hook, but you can wrap resolvers or use the `execute`
function directly to insert instrumentation.
### Use tracing tools
Distributed tracing shows how a request flows through services and where time is spent. This
is especially helpful for debugging performance issues.
GraphQL.js allows you to hook into the execution pipeline using:
- `execute`: Trace the overall operation
- `parse` and `validate`: Trace early steps
- `formatResponse`: Attach metadata
Tracing tools that work with GraphQL include:
- [Apollo Studio](https://www.apollographql.com/docs/studio/)
- [OpenTelemetry](https://opentelemetry.io)
## Handle errors
How you handle errors in production affects both security and client usability. Avoid exposing
internal details in errors, and return errors in a format clients can interpret consistently.
For more information on how GraphQL.js formats and processes errors, see [Understanding GraphQL.js Errors](./graphql-errors/).
### Control what errors are exposed
By default, GraphQL.js includes full error messages and stack traces. In production, you may want
to return a generic error to avoid leaking implementation details.
You can use a custom error formatter to control this:
```js
import { GraphQLError } from 'graphql';
function formatError(error) {
if (process.env.NODE_ENV === 'production') {
return new GraphQLError('Internal server error');
}
return error;
}
```
This function can be passed to your server, depending on the integration.
### Add structured error metadata
GraphQL allows errors to include an `extensions` object, which you can use to add
metadata such as error codes. This helps clients distinguish between different types of
errors:
```js
throw new GraphQLError('Forbidden', {
extensions: { code: 'FORBIDDEN' },
});
```
You can also create and throw custom error classes to represent specific cases, such as
authentication or validation failures.
## Manage your schema safely
Schemas evolve over time, but removing or changing fields can break client applications.
In production environments, it's important to make schema changes carefully and with clear
migration paths.
### Deprecate fields before removing them
Use the `@deprecated` directive to mark fields or enum values that are planned for removal.
Always provide a reason so clients know what to use instead:
```graphql
type User {
oldField: String @deprecated(reason: "Use `newField` instead.")
}
```
Only remove deprecated fields once you're confident no clients depend on them.
### Detect breaking changes during deployment
You can compare your current schema against the previous version to detect breaking changes.
Tools that support this include:
- [`graphql-inspector`](https://github.com/graphql-hive/graphql-inspector)
- [`graphql-cli`](https://github.com/Urigo/graphql-cli)
Integrate these checks into your CI/CD pipeline to catch issues before they reach production.
## Use environment-aware configuration
You should tailor your GraphQL server's behavior based on the runtime environment.
- Disable introspection and show minimal error messages in production.
- Enable playgrounds like GraphiQL or Apollo Sandbox only in development.
- Control logging verbosity and other debug features via environment flags.
Example:
```js
const isDev = process.env.NODE_ENV !== 'production';
app.use(
'/graphql',
graphqlHTTP({
schema,
graphiql: isDev,
customFormatErrorFn: formatError,
})
);
```
## Production readiness checklist
Use this checklist to verify that your GraphQL.js server is ready for production.
Before deploying, confirm the following checks are complete:
### Build and environment
- Bundler sets `process.env.NODE_ENV` to `'production'`
- Development-only checks are removed from the production build
### Schema security
- Authentication is required for requests
- Authorization is enforced via business logic
- Rate limiting is applied
- Only allow trusted documents, or:
- Introspection is disabled or restricted in production
- Query depth is limited
- Query cost limits are in place
### Performance
- `DataLoader` is used to batch data fetching
- Expensive resolvers use caching (request-scoped or shared)
- Public queries use HTTP or CDN caching
- Schema is reused across requests (not rebuilt each time)
### Monitoring and observability
- Logs are structured and machine-readable
- Metrics are collected (e.g., with Prometheus or OpenTelemetry)
- Tracing is enabled with a supported tool
- Logs do not include sensitive data
### Error handling
- Stack traces and internal messages are hidden in production
- Custom error types are used for common cases
- Errors include `extensions.code` for consistent client handling
- A `formatError` function is used to control error output
### Schema lifecycle
- Deprecated fields are marked with `@deprecated` and a clear reason
- Schema changes are validated before deployment
- CI/CD includes schema diff checks
### Environment configuration
- Playground tools (e.g., GraphiQL) are only enabled in development
- Error formatting, logging, and introspection are environment-specific