All notes

Timeouts Must Share One End-to-End Budget

Include queueing, response bodies, retries and cancellation in the same task deadline.

English edition, edited for length. Read the complete Chinese original.

A single page calls three APIs sequentially, each with a timeout of 10 seconds. This means the user might have to wait significantly longer than the timeout of a single API. The timeouts should not be just a set of unrelated constants, but should be derived from the overall deadline of the user’s task.

This article uses fetch to call upstream services. The numbers provided are merely example configurations and do not represent recommended production values. Actual budget should be determined based on service objectives, network conditions, and dependencies.

Allocating a Budget for User Tasks

Assume the search interface first reads the index, then adds a summary, and finally generates a response. The overall waiting time is a single constraint, not the maximum value achieved by each of the three steps. To preserve the space for response serialization and network return, and to allocate necessary upstream and optional supplementary information, the index must be available to complete the search. If the summary is unavailable, it may still be possible to return the title and link. Only after determining the necessary information and exhausting the budget can we decide whether to fail or degrade.

The overall deadline can be determined from the start of the request, with each step calculating the remaining time without constantly creating new complete windows. If the caller provides a shorter deadline, it is necessary to verify the source and boundaries, and then take the earlier deadline. It is not allowed to allow arbitrary input to extend the system’s upper limit. When passing deadlines between services, consider clock differences, and prioritize using APIs suitable for monotonic timing for local measurements. Calendar time is suitable for recording events, but may not be suitable for calculating durations.

Assuming the first step has already consumed a significant portion of the budget, even the second step, which is typically quick, cannot monopolize the original total duration. Options include skipping optional additions or using the remaining time. The numbers should come from real service goals and measurements; the example is only for illustration and does not recommend a fixed three-second duration for all products. The user’s tolerance for waiting and the platform’s allowed execution time are also constraints. The application must express the results within these limits.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
try {
  const response = await fetch(upstreamURL, {
    signal: controller.signal,
  });
  return await handleResponse(response);
} finally {
  clearTimeout(timer);
}

This section handles the timeout processing for the timer, but it’s important to ensure that handleResponse continues to read the complete content and that the cancellation is passed on to subsequent requests. The timeout should only be cleared after receiving the response header, as this could lead to the content download getting stuck in an infinite wait. The application also has its own limitations, and the timeout should be handled in a way that provides a clear error message before the platform terminates.

Multiple calls can share a common signal, while independent calls can be set with shorter budgets. Parallel calls reduce serial waiting but increase upstream concurrency and resource usage. Only tasks that are truly independent are suitable for parallel execution; business dependencies should not be disrupted for the sake of speed.

Cancelling a signal requires traversing the complete processing stage.

The fetch API does not guarantee that the response headers are fully processed before the body is considered read. If further processing, such as JSON parsing, cancellation signals, and timeouts, are applied after the body is read, they should still take effect during the reading phase. If the body is passed to a client-side stream, a separate and clear stream lifecycle strategy is required. Do not immediately clear the timer after retrieving the headers, but instead, wait indefinitely for the slow body. For operations or library calls that do not support cancellation, the AbortController will not automatically terminate the code; instead, you need to check for cancellation or implement a cancellable boundary.

Multiple parallel requests can share a common signal while also having their own sub-periods. When combining signals, use standard capabilities or established encapsulation supported by the runtime, preserving the reason for cancellation and cleaning up listeners to avoid leaving a large number of callbacks after a request completes. Whether a platform supports a specific AbortSignal method should be determined by checking against the runtime’s capabilities; for the sake of brevity, we are not using unverified capabilities in the example. Timers, connections, and readers must be released after successful or failed operations.

Parallel strategies are suitable for independent reads and should not allow dependent operations to execute prematurely. Three parallel upstream operations reduce wall-clock time, but increase instantaneous concurrency and cost, potentially triggering upstream rate limiting. For a hot endpoint, limiting concurrency or caching public results may be more effective than shortening the duration of each request. Budget and resource control should be designed together to avoid turning local speed optimizations into global overload.

Retry attempts must adhere to the same budget

After the initial waiting period, a complete retry should be performed multiple times, which will significantly increase the total duration. Before retrying, calculate the remaining time, and use a limited number of retries with a fallback mechanism. Also, distinguish between temporary network failures, upstream rate limiting, and errors that cannot be retried.

GET is generally better suited for safe retries, while POST may result in duplicate writes. If the business supports idempotent keys, their semantics and expiration should be defined by the upstream system, not arbitrarily added by the client. Be especially cautious with timeout write operations: the client doesn’t know if the operation has been successful, and blind retries may result in duplicate charges or record creation.

After the initial network failure, the remaining budget must accommodate both fallback and a subsequent attempt. Before retrying, determine whether it’s worthwhile to proceed, and if only a very short time remains, return a controlled error immediately. Limited exponential backoff with jitter can reduce the peak of synchronous retries, but not all errors can be retried. Authentication failures, invalid input, and business-level rejections typically require fixing rather than prolonged waiting. When rate limiting, handle according to the protocol and remaining time, and do not unconditionally sleep for the entire Retry-After period.

GET does not guarantee that the server-side implementation has no side effects; the HTTP semantics should be followed by the interface contract. The concept of request timeouts is particularly ambiguous: the server may have completed the transaction, but the response has not yet arrived. To support idempotency in the upstream, use stable operation identifiers and the same payload. Repeating requests should result in the same business outcome. Clients should generate new keys each time they retry, which loses the meaning of idempotency. When the upstream does not support idempotency, the client should query the status or allow the user to explicitly handle it.

Record the number of retries and the reasons for them as metrics, rather than treating all exception stacks as categories. Observe the success rate after retries and the additional load. If most retries still fail, they may only exacerbate the problem. Circuit breakers or fallback mechanisms require their own recovery conditions and cannot permanently block recovered upstream systems or retry every request. Complex mechanisms should only be implemented if they are proven necessary by actual failure patterns.

Both the generic HTTP client, business services, and entry handler can be configured with timeouts. If each independently retries, the total number of requests significantly exceeds the apparent configuration due to the multiplication of retry counts across the three layers. It’s crucial to clearly identify which layer handles retries and which layer only propagates cancellation, to prevent multiple libraries from silently restarting tasks. The default timeouts and retry settings of third-party SDKs should be carefully checked, as it’s incorrect to assume that a business code with only one fetch call will result in a total of one call.

Regarding the upstream data for tracking the number of attempts used in testing, we can analyze how many requests are issued for each overall task and when they are cancelled. If requests are still received after cancellation, it indicates that a particular layer is not adhering to the signal or is retrying in the background. To accurately identify the true bottleneck, it’s necessary to separately record the duration and reason for both required and optional calls. Simply increasing the overall budget may only prolong user waiting times without improving success rates.

The rollback results and caching mechanisms must avoid permanently masking failures. Record the degradation ratio and age each time a successful fallback to the old value occurs, and trigger an alarm if the conditions are exceeded. After recovery, verify that the data is being read from the new data source. Outdated results that have not been approved by the business should not automatically appear, especially regarding permissions and transaction status. The budget design should ultimately be able to answer questions such as why a task ended, whether resources have stopped, and how recent the returned data is, rather than simply demonstrating that a timer has been triggered.

Don’t just swallow your mistakes.

AbortError or timeout errors should be categorized separately from DNS failures, upstream 500 errors, and parsing failures. Public responses should not expose internal stacks, but should provide a stable state and a corresponding ID. Do not return empty results after catching an error, as this would make timeout errors appear as if there is “no data”.

When a user disconnects, it’s important to minimize unnecessary activity. While certain essential background tasks can continue running through the platform’s mechanisms, this should be a deliberate business decision, not a situation where every failed request leaves behind an endless stream of background tasks.

Once a user leaves the page, normal read operations can be stopped. Business write operations that have been accepted may need to be completed and the results saved, in order to be queried later. It is not possible to arbitrarily terminate a payment database transaction after the browser disconnects, nor should each read request continue to execute expensive upstream operations. The meaning of “cancel” should be defined based on the stage of the operation, distinguishing between canceling a wait and undoing a business operation, although these are not always the same.

Public responses can provide timeout categories and associated IDs, indicating which stage of the process exhausted the budget. If the user cancels proactively, it may not be necessary to count this as a service availability error. If the service continues to timeout, an alert should be triggered. The reason for cancellation helps to explain the distribution of delays and prevents incorrectly recording already canceled tasks as successfully completed. The logs do not contain the complete user request body, and the stage and time information are sufficient to identify most issues.

The timed test uses a controlled, slow upstream, allowing for reasonable scheduling errors, and consistently targets a specific millisecond. The total duration is checked to ensure it falls within the specified range, with no additional retries, and the timer is released. For downstream processes that cannot be cancelled, it is also observed whether they continue to consume resources after the timeout response. The fact that the user received an error while the background work remained in a long queue is evidence that the budget target was not met due to resource constraints.

Using a Time Log to Verify if a Budget is Actually Shared

Assume that the total task budget already includes the index waiting time. The remaining portion needs to accommodate summarization, result serialization, and return. The time log records the start, end, remaining budget, and reason for completion for each stage. Sub-requests receive the currently available time limit, not the original total duration. Parallel optional tasks are also subject to the same overall deadline. Once a task is completed, it cannot be reopened for other tasks. The numbers are only used to explain the log, not as a general recommendation.

The timing should be performed on a consistent, unchanging clock. The event log should then record the time displayed on the wall clock. When transferring absolute deadlines between services, it’s crucial to be aware of clock discrepancies and rely on trusted inputs. The recipient should not allow any client to request an arbitrarily extended deadline. It’s important to clearly define how the local system’s maximum value and the caller’s remaining value are determined, taking the shorter of the two. A deadline is a cooperative agreement, not an unlimited execution time that can be arbitrarily requested by a client.

Serialization and parsing also consume resources. After fetch returns the headers, the slow parsing of the body, large JSON objects, and synchronous computations may still take time. Setting size limits for the body and checking if the computation library supports interruption are important. If it doesn’t, the AbortController only signals the cancellation, without interrupting the synchronous code. Report the time of the cancellation request and the actual completion time, distinguishing between “the user is no longer waiting” and “the resource is no longer being used.”

How to Amplify Failure with Multiplied Retry Attempts

The entry point allows for three attempts, with the business library attempting three retries, the SDK attempting three retries, and the overall call count potentially continuing to multiply. The example is not to say that all libraries use three attempts, but rather to remind that the overall call graph must be reviewed. Assign a unique retry owner to each operation, and other layers should propagate errors or explicitly disable default retries. The overall timeout should still be maintained to prevent a single attempt from exceeding the task budget.

In the upstream, during failures, retry mechanisms can increase queuing and lead to more requests timing out. While limited retries and jitter reduction can help with synchronization, they don’t create capacity. Observe the success rate, additional request volume, and the time it takes for requests to timeout after retries, and then determine whether retrying is worthwhile. Authentication errors and invalid input directly fail, and rate limiting continues based on the “Retry-After” header and remaining time, rather than waiting indefinitely when only a small amount of capacity remains.

Test upstream counting attempts and control the number of results generated each time. Assert that a complete task can only issue a maximum number of approvals, and that retries are included within the overall deadline. After cancellation, a delayed retry should not be initiated. If the SDK cannot accept the signal, at least understand whether it is still functioning and limit its concurrency and lifespan. It is not sufficient to simply declare that the downstream task is complete when the timeout is reached in the outer Promise.race. The fact that a task has stopped is not the same as the promise no longer being waited for.

How to Ensure Output Remains Recognizable if Optional Steps Fail

The search title and link are already available. When the summary cannot be added, the legal structure without the summary can be returned. Optional fields are clearly defined in the type, and the template does not interpret missing fields as empty content or an article not found. If using an old summary, the source time and degradation indicator are retained, and the product is expressed according to the needs. The allowed range for reading public information cannot be directly applied to permissions, prices, or transaction status.

Once parallel optional steps exceed their deadlines, they can be stopped and continue generating necessary results. If the overall necessary steps fail, all other unnecessary reads can be stopped. allSettled can collect results, but if it continues to wait for all cancellable tasks, it may still exceed the user’s budget. Choose the aggregation mechanism based on dependencies and cancellation capabilities, and do not treat a Promise method name as a complete and reliable solution.

When degradation continues to occur, an independent alert is triggered because the front-end might mask long-term upstream failures. Recovery tests confirm that new data reappears, and old degradation markers are not permanently stored in the cache. Unlike error swallowing, which requires type, source, duration, and observation, degradation detection provides a simple, seemingly successful, empty object.

When writing timeouts occur, query rather than guess the result.

Users submit tasks, which may already have been submitted if the upstream process times out. The client retains operation identifiers and queries the status. If the upstream process supports idempotency, the same request is repeated using the same identifier and payload. Each retry generates a new key, creating a new operation. If the upstream process does not support reliable result queries, the interface indicates that the result is unknown and provides a controlled handling path, without promising “Not executed, so you can safely retry.”

The client disconnecting should not also involve arbitrarily canceling already accepted critical business transactions. The server should continue processing and record the results based on the stage of the operation, which can then be queried by the user. Normal searches can typically be stopped. The user’s waiting for cancellation, network transmission cancellation, and business rollback should be modeled separately. The button text and interface contracts should accurately reflect the actual capabilities, and should not misrepresent the AbortSignal as a means of undoing all side effects.

When the response header arrives quickly but the body remains slow, it indicates that the timer is being released prematurely. To check if the parsing stage is protected by a size budget when using an endpoint that returns a large, valid JSON response, the timeout signal may not necessarily interrupt the synchronous parsing. A possible solution is to reduce the allowed volume, adopt a streaming protocol, or move the computation to a task that can be interrupted, rather than adding a shorter constant to the fetch operation.

Furthermore, it’s crucial to ensure that the exception handling doesn’t mask the original cause. Cleanup operations within the finally block, such as closing connections or logging, may also fail, leading to misclassification of the exception if a new exception overrides the initial timeout. The controlled error path should preserve the primary failure and record necessary secondary categories, without simply ignoring all errors to ensure a smooth return. The interpretability of the timeout implementation also encompasses these final stages.

It’s important to distinguish between idle periods and total periods when implementing streaming responses.

The upstream periodically sends small data chunks, and the idle timer is constantly reset. This allows the download to last for a long time. The idle period limit sets the interval between two available progress updates, while the total period limit sets the cumulative time a task can consume resources. These two limits cannot be used interchangeably. Additionally, a total byte limit is needed to prevent bypassing the initial size check by sending small chunks continuously.

The controlled fixtures simulate long periods without data, intermittent small chunks, and rapid bursts of large data, to determine which constraint triggers the error. Once some data has been sent to the client, it is no longer possible to feign a completely new, valid JSON error. The protocol must define how a stream interruption should be indicated as incomplete, and consumers must not treat incomplete content as a complete result. This discussion focuses on the acceptance design, and does not claim that all fetch wrappers provide this set of constraints.

Conduct Faulty Experiment Checks Focusing on the Tail End, Not Just Response Time

Implement slow-down for upstream requests, slow-down for the main body, and disconnection after successful completion, and measure the total response time and the duration of calls that continue after cancellation. Allow for minor runtime scheduling errors near the deadline, aiming for precision to the millisecond; verify that no new attempts, timers, or listeners are created, and that resources return to an acceptable state. Compare against scenarios with no retries or limited retries, reporting successful, delayed, and additional load, while ensuring that the average response time is improved.

The overall budget provides for a predictable and controlled project. External systems remain uncertain, and the potential for negative or unforeseen outcomes still exists. A clear and accurate timeline, along with a comprehensive recovery agreement, provides a better explanation for why the project ended than constantly adjusting three separate variables.

Slots that are waiting to be executed also consume the same budget.

The interface sends the request to the concurrency limiter, which queues it for two seconds before initiating the fetch. If the timeout occurs, the timer is reset from the point of acquiring the slot. During busy periods, the user’s waiting time exceeds the surface budget, while idle tests run normally. The overall deadline is determined at the beginning of the task, and the queue waiting, connection acquisition, and network calls all consume it. Once a slot is acquired, the remaining time is recalculated. If there is no remaining time, no further upstream requests are sent.

The waiting queue needs a mechanism to remove tasks. If a user has left and a task remains in the queue, waiting for a slot to become available and execute, it will lead to uncontrolled execution and wasted resources. The controlled executor should occupy a unique test slot, add a second task and cancel it, then release the slot. The upstream counter should not increase, and the waiting state and listener should be cleared. This example validates the cancellation process during the entry phase, not the normal fetch cancellation.

Both the maximum queue length and the maximum execution limit are also defined. While unlimited queuing reduces the number of concurrent calls, it increases memory usage, waiting times, and expired tasks. When the queue exceeds the approved capacity, a clear and understandable “busy” result is returned, and the queue is not re-queued multiple times. The metrics distinguish between queue rejections, queue timeout expiration, and actual call timeouts, allowing you to determine whether to adjust concurrency, capacity, or upstream budget. This system does not measure actual throughput; the thresholds still need to be determined experimentally by the target system.

References: AbortController, Workers fetch.