Production engineering guide for request coalescing covering patterns, implementation strategies, and operational best practices.
Request coalescing is a backend pattern for reducing redundant network work by merging multiple similar incoming requests into a single, shared computation. It matters when multiple clients or services issue overlapping or identical queries in rapid succession—such as a user dashboard loading simultaneously across tabs, or a microservice fetching user profiles for a batch of IDs—and the underlying data source is expensive to compute, I/O-bound, or both.
Coalescing is most valuable when the cost of executing a request exceeds the cost of coordinating the coalescing logic. This typically occurs when:
For instance, if ten clients request the same user profile within 200ms of each other, coalescing ensures only one database query runs, serving all ten requests with a single result.
At its heart, request coalescing involves a central coordinator that tracks incoming requests and delays execution until a window of time or a threshold of requests is met. The coordinator maintains a registry of pending requests, keyed by a unique identifier derived from the request's parameters.
The coordinator proceeds in three phases:
The key is that the identifier used to group requests must be stable and representative of the request’s intent.
Requests are coalesced based on a request key, a canonical representation of the request’s parameters. The most common approach uses a hash of the request’s parameters, typically including:
For example, a request to /api/users/123?include=posts&format=compact and a request to /api/users/123?include=posts&format=compact&locale=en should be coalesced if the format and locale headers are part of the key.
In practice, request keying often involves a function like:
func RequestKey(r *http.Request) string {
var buf strings.Builder
buf.WriteString(r.Method)
buf.WriteString(r.URL.Path)
query := r.URL.Query()
keys := make([]string, 0, len(query))
for k := range query {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
buf.WriteString(k)
for _, v := range query[k] {
buf.WriteString(v)
}
}
// Include headers that affect behavior
for _, h := range []string{"Accept", "Content-Type", "X-User-ID"} {
if value := r.Header.Get(h); value != "" {
buf.WriteString(h)
buf.WriteString(value)
}
}
// Include body if present
if r.Body != nil {
body, _ := io.ReadAll(r.Body)
buf.Write(body)
}
return sha256.Sum256([]byte(buf.String()))
}
This function produces a unique key for each request. The coordinator uses this key to match incoming requests.
Coalescing is governed by two parameters: timeout and threshold.
These parameters are configurable and often applied per endpoint or service.
A common configuration in a JSON-based config file (e.g., config.yaml):
coalescing:
- endpoint: "/api/users/{id}"
timeout: 100ms
threshold: 3
cache_ttl: 1s
max_concurrent: 100
- endpoint: "/api/orders"
timeout: 250ms
threshold: 5
cache_ttl: 5s
max_concurrent: 50
This config instructs the system to:
/api/users/{id} into a single query if at least three requests arrive within 100ms./api/orders when five requests arrive within 250ms.The cache_ttl field determines how long the computed result is available for subsequent requests, even after the coalescing window has closed.
Consider a service that computes user profiles. Clients send requests like:
GET /api/users/42?include=posts&format=compact HTTP/1.1
Host: api.example.com
Accept: application/json
X-User-ID: 100
The coalescing coordinator starts with a timeout of 100ms and threshold of 3.
GET:/api/users/42?include=posts&format=compact.If no new requests arrive by T = 200ms, the job completes. If a new request arrives at T = 205ms, it starts a new job.
locale or format headers are not coalesced, even though they could be.?include=posts vs ?include=posts&) are not merged.A frequent mistake is forgetting to include a header that significantly affects the computation. For example, a X-User-ID header that influences caching and query logic is often omitted from the key, leading to missed coalescing.
When coalescing windows overlap, the result from an earlier window may be outdated by the time a later window starts.
For instance, if the database is updated between T = 100ms and T = 150ms, and a new coalescing job starts at T = 130ms, the result from the first job may not reflect the latest data.
This leads to stale coalescing results—clients receive responses computed from outdated data.
Solutions include:
If the coalescing timeout is longer than the client’s request timeout, clients may time out before receiving a response.
For example, a client has a 500ms timeout, but the coalescing window is 250ms. If the coordinator starts execution at T = 200ms, the client may time out at T = 500ms, but the response arrives at T = 300ms—so the client waits an extra 200ms.
To mitigate this:
Location header pointing to the final result.HTTP/1.1 202 Accepted
Location: /api/users/42?etag=abc123
Content-Length: 0
Clients can poll the Location URI for the final result.
A coalescing job that starts at T = 0ms but receives no new requests until T = 1000ms may be idle for 900ms. If the service is under low load, this job may be wasted.
To avoid this:
Jobs that have not received a new request for longer than timeout * 1.5 may be considered “stale” and can be merged or preempted.
After execution, the coalesced result is typically cached. The cache key includes the request key and the execution timestamp.
A common mistake is forgetting to invalidate the cache when the underlying data changes.
For example, if a user’s profile is updated after a coalescing job finishes, clients may receive outdated data.
Solutions:
ETag: "v2-user-42-2024-05-15T10:00:00Z"
Cache-Control: public, max-age=300, s-maxage=600
Clients can use If-None-Match to fetch updated results.
Request coalescing works well with:
In a typical deployment:
curl) to compute the result:curl -X POST \
-H "Content-Type: application/json" \
-d '{"user_id": 42, "include": ["posts"], "format": "compact"}' \
http://profile-service/api/compute-profile
When the coalescing execution fails, the coordinator must propagate the error to all waiting clients.
Failure modes include:
The coordinator returns a 503 Service Unavailable with a payload:
{
"error": "computation_failed",
"message": "Failed to compute user profile for user 42",
"details": {
"error_code": "DB_TIMEOUT",
"timestamp": "2024-05-15T10:00:00Z",
"retry_after": 5000
}
}
Clients can:
If the failure is transient, the coordinator may replay the failed job, merging it into a new window.
Request coalescing is a powerful, predictable pattern for reducing redundant work in high-load systems. Its success depends on careful request keying, appropriate timeout and threshold configuration, and robust error handling. When implemented correctly, it delivers significant performance gains with minimal infrastructure. But its subtleties—stale results, keying mismatches, and timing drifts—are often the source of silent degradation in production.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy