1. Introduction
The Grails Server Timing plugin adds Server Timing HTTP headers to your Grails application responses. This allows you to see detailed server-side performance metrics directly in your browser’s developer tools.
1.1. What is Server Timing?
The Server-Timing header is part of the W3C Server Timing specification that enables servers to communicate performance metrics about the request-response cycle to the client.
These metrics can include:
-
Total request processing time
-
Database query durations
-
Controller action execution time
-
View rendering time
-
Any custom metrics you want to track
1.2. Why Use This Plugin?
When developing and debugging Grails applications, understanding where time is spent during request processing is crucial. This plugin automatically captures:
-
Action Timing - How long your controller action takes to execute
-
View Timing - How long GSP/view rendering takes
-
Total Timing - The complete request lifecycle
-
Other Timing - Time spent on non-controller requests (static assets, etc.)
All of this information is exposed via the standard Server-Timing header, which modern browsers display in their Network developer tools panel.
1.3. Browser Support
The Server Timing header is supported by all modern browsers:
-
Chrome 65+
-
Firefox 61+
-
Edge 79+
-
Safari 16.4+
-
Opera 52+
1.4. Quick Start
Add the plugin dependency to your build.gradle:
dependencies {
implementation 'org.grails.plugins:grails-server-timing:0.0.2-SNAPSHOT'
}
That’s it!
The plugin is automatically enabled in development and test environments only.
No additional configuration is required to get started.
| For security reasons, the plugin is disabled by default in production. Server Timing headers can expose timing information that may be useful to attackers. See the Server Timing Specification and Configuration sections for more details. |
2. Server Timing Specification
The Server-Timing header is a standardized way for servers to communicate performance metrics to clients.
This section provides an overview of the specification and how this plugin implements it.
2.1. W3C Server Timing Specification
The Server Timing header is defined by the W3C Server Timing specification. This specification is a W3C Working Draft that enables servers to communicate performance metrics about the request-response cycle.
2.2. Header Format
The Server-Timing header follows this format:
Server-Timing: metric-name;dur=duration;desc="description",...
2.2.1. Components
-
metric-name (required): A token identifying the metric. Must follow RFC 7230 token rules (alphanumeric plus certain special characters).
-
dur (optional): Duration in milliseconds (can include decimals).
-
desc (optional): A human-readable description enclosed in quotes.
2.2.2. Examples
Single metric:
Server-Timing: db;dur=53.2;desc="Database query"
Multiple metrics:
Server-Timing: db;dur=53.2;desc="Database",render;dur=47.1;desc="Template rendering"
Metric without duration (for presence indication):
Server-Timing: cache-hit
| This plugin only includes metrics that have durations. Metrics without durations (used for presence indication) are not currently supported. |
2.3. MDN Documentation
For detailed browser-side documentation, see the MDN Server Timing documentation.
2.4. Security Considerations
The specification includes important security considerations:
2.4.1. Timing Attacks
Detailed timing information can potentially be used in timing attacks to infer information about server-side operations. The specification recommends:
-
Only exposing timing information in development/staging environments
-
Using appropriate access controls if exposing in production
-
Being cautious about what metrics are exposed
Due to these security considerations, this plugin is only enabled by default in development and test environments.
In production, the plugin is disabled unless explicitly enabled via configuration.
See the Configuration section for details on how to change this behavior.
|
2.4.2. Cross-Origin Requests
By default, Server Timing information is only available to same-origin requests.
For cross-origin requests, the server must include the Timing-Allow-Origin header:
Timing-Allow-Origin: https://example.com
Or to allow all origins (use with caution):
Timing-Allow-Origin: *
This plugin does not automatically add Timing-Allow-Origin.
If you need cross-origin access to timing data, you must configure this header separately.
|
2.5. Related Specifications
-
Navigation Timing Level 2 - Client-side navigation timing
-
Resource Timing Level 2 - Client-side resource timing
-
RFC 7230 - HTTP/1.1 Message Syntax and Routing
3. Configuration
The Grails Server Timing plugin is designed to work out-of-the-box with sensible defaults, but can be customized to fit your needs.
3.1. Default Behavior
By default, the plugin is automatically enabled in development and test environments and disabled in production.
This ensures that performance timing information is available during development without exposing potentially sensitive timing data in production.
3.2. Configuration Options
All configuration options are specified in your application.yml or application.groovy file under the grails.plugins.serverTiming namespace.
3.2.1. Enabling/Disabling the Plugin
grails:
plugins:
serverTiming:
enabled: true # or false
| Property | Type | Default | Description |
|---|---|---|---|
|
|
|
When |
3.2.2. Metric Key
The plugin stores timing metrics in the HTTP request attributes. You can customize the attribute key if needed:
grails:
plugins:
servertiming:
metricKey: MyCustomTimingKey
| Property | Type | Default | Description |
|---|---|---|---|
|
|
|
The request attribute key used to store the |
3.3. Environment-Specific Configuration
You can configure the plugin differently per environment:
environments:
development:
grails:
plugins:
serverTiming:
enabled: true
test:
grails:
plugins:
serverTiming:
enabled: true
production:
grails:
plugins:
serverTiming:
enabled: false
3.4. Enabling in Production
| Enabling Server Timing in production may expose timing information that could be useful to attackers. Only enable in production if you understand the security implications and have appropriate access controls in place. |
If you need to enable timing headers in production (for example, behind an authenticated admin interface or internal network), you can do so:
environments:
production:
grails:
plugins:
serverTiming:
enabled: true
Consider using conditional logic or feature flags to enable timing only for specific users or requests in production environments.
4. Using Browser Developer Tools
One of the primary benefits of the Server Timing header is that modern browsers automatically display this information in their developer tools.
4.1. Viewing Server Timing in Chrome
-
Open Chrome Developer Tools (F12 or Ctrl+Shift+I / Cmd+Option+I)
-
Navigate to the Network tab
-
Make a request to your Grails application
-
Click on the request in the list
-
Select the Timing tab in the details panel
You’ll see a breakdown of the request timing, including the custom metrics from this plugin displayed under "Server Timing".
4.2. Viewing Server Timing in Firefox
-
Open Firefox Developer Tools (F12 or Ctrl+Shift+I / Cmd+Option+I)
-
Navigate to the Network tab
-
Make a request to your Grails application
-
Click on the request in the list
-
Select the Timings tab in the details panel
The Server Timing metrics appear at the bottom of the timing breakdown.
4.3. Viewing Server Timing in Safari
-
Open Safari Web Inspector (Cmd+Option+I)
-
Navigate to the Network tab
-
Make a request to your Grails application
-
Click on the request in the list
-
Look for the Server Timing section in the timing details
4.4. Understanding the Metrics
When viewing the Server Timing data for a Grails controller request, you’ll typically see:
| Metric | Description |
|---|---|
|
The complete request processing time from when the filter receives the request until the response is committed. |
|
Time spent executing your controller action code. This includes any service calls, database queries, or other logic in your action method. |
|
Time spent rendering the GSP view or other response template. This starts after your action returns and ends when the view is fully rendered. |
|
For non-controller requests (like static assets), this captures the processing time. For controller requests, this metric is stopped when the action begins. |
4.5. Example Header Value
A typical Server-Timing header from a Grails controller request looks like:
Server-Timing: total;dur=156.3;desc="Total",action;dur=45.2;desc="Action",view;dur=98.7;desc="View"
This tells you:
-
Total request time: 156.3ms
-
Controller action execution: 45.2ms
-
View rendering: 98.7ms
4.6. Interpreting Results
4.6.1. Slow Action Time
If action timing is high, investigate:
-
Database queries
-
External API calls
-
Complex business logic
-
Service method performance
4.6.2. Slow View Time
If view timing is high, investigate:
-
Complex GSP logic or loops
-
Many template includes
-
Large data sets being rendered
-
Inefficient tag library usage
4.7. Programmatic Client Side Access
You can access Server Timing data programmatically in JavaScript using the Performance API:
const entries = performance.getEntriesByType('navigation');
entries.forEach(entry => {
if (entry.serverTiming) {
entry.serverTiming.forEach(timing => {
console.log(`${timing.name}: ${timing.duration}ms`);
});
}
});
5. How It Works
This section explains the architecture of the plugin and how it handles different types of requests.
5.1. Architecture Overview
The plugin consists of three main components that work together:
-
ServerTimingFilter - A servlet filter that wraps all HTTP responses
-
ServerTimingInterceptor - A Grails interceptor that tracks controller action and view timing
-
ServerTimingResponseWrapper - A response wrapper that ensures headers are added before the response is committed
5.2. Request Flow
5.2.1. Controller Requests
When a request hits a Grails controller action, the following sequence occurs:
1. ServerTimingFilter receives request
- Creates TimingMetric object
- Starts 'total' timer
- Starts 'other' timer
- Wraps response with ServerTimingResponseWrapper
2. ServerTimingInterceptor.before() is called
- Starts 'action' timer
3. Controller action executes
- Your business logic runs here
4. ServerTimingInterceptor.after() is called
- Stops 'action' timer
- Starts 'view' timer
5. View/GSP renders
6. Response is committed
- ServerTimingResponseWrapper intercepts the commit
- Removes 'other' timer if an action/view was invoked, otherwise stops it
- Stops 'action' timer if still running (edge case: action committed the response directly)
- Stops 'view' timer if still running
- Stops 'total' timer
- Adds Server Timing header to response
5.2.2. Static Resources and Other Requests
For requests that do not hit a Grails controller (static assets, images, CSS, JavaScript, etc.):
1. ServerTimingFilter receives request
- Creates TimingMetric object
- Starts 'total' timer
- Starts 'other' timer
- Wraps response with ServerTimingResponseWrapper
2. ServerTimingInterceptor is NOT called
- 'action' and 'view' timers are never created
3. Asset pipeline or other handler processes request
4. Response is committed
- ServerTimingResponseWrapper intercepts the commit
- Stops 'other' timer
- Stops 'total' timer
- Adds Server Timing header to response
5.3. The Response Wrapper
A key technical challenge with Server Timing is that HTTP headers must be sent before the response body. However, we do not know the final timing values until after the view has rendered.
The ServerTimingResponseWrapper solves this by wrapping the original HttpServletResponse and deferring the
Server-Timing header injection until the last possible moment — just before the first byte of body content is
written. This maximizes timing accuracy because all metrics (action, view, etc.) have as much time as possible to
accumulate before the header value is computed and frozen.
5.3.1. Deferred Header Injection
Both output paths use the same deferred strategy:
-
getOutputStream()returns aServerTimingServletOutputStreamthat interceptswrite(),flush(), andclose(). On the first call, it triggers header injection before delegating to the underlying stream. -
getWriter()returns aServerTimingPrintWriterthat interceptswrite(),flush(), andclose()in the same way. All higher-levelPrintWritermethods (print(),println(),printf(), etc.) route through the overriddenwrite()methods, so they are also covered.
In both cases, subsequent writes pass through directly without any overhead.
5.3.2. Other Commit Points
The wrapper also intercepts methods that commit the response without writing body content:
-
sendError()— error responses -
sendRedirect()— redirect responses -
flushBuffer()— explicit buffer flushes
These inject the header eagerly since the response is being committed immediately.
5.3.3. Safety Net
As a final safeguard, the ServerTimingFilter calls beforeCommit() in a finally block after the filter chain
completes. This handles edge cases where no output was written (e.g., 204 No Content responses). The header
injection is idempotent — a boolean flag ensures it only runs once regardless of how many commit points are hit.
5.4. Metric Descriptions
Each metric in the Server-Timing header includes:
-
name - A short identifier (e.g.,
action,view,total) -
dur - Duration in milliseconds
-
desc - A human-readable description
| This plugin only includes metrics that have been started and stopped (i.e., have a duration). Metrics that were created but never started, or started but never stopped, are not included in the header. |
Example:
Server-Timing: action;dur=45.2;desc="Action",view;dur=98.7;desc="View"
5.5. Controller vs Non-Controller Requests
The plugin handles different request types automatically:
| Request Type | Metrics Captured | Example |
|---|---|---|
Controller with view |
|
|
Controller with render |
|
|
Static assets |
|
|
Other resources |
|
|