The Reactive Manifesto: Staying Responsive under Failure and Load
By Sergey Nosov
16 October 2026
Suppose a checkout service calls a pricing service whenever it shows a price. Pricing usually answers in 40 milliseconds. One afternoon a slow query makes it take 20 seconds, and checkout has no timeout on the call. Every request that needs a price now waits, holding memory, often a connection, and, in a thread-per-request server, a thread. If the waits use up something the other pages share, such as a connection pool, pages that never needed a price stop loading too, as Microsoft’s guide to the Bulkhead pattern warns. Nothing in checkout failed. It waited.
The Reactive Manifesto describes systems that do not break this way. It asks for four traits: responsive, resilient, elastic, and message driven. The list is easy to recite and easy to misread as four separate features. Read closely, it names one goal, the two conditions under which the goal must still hold, and the means that makes both possible. The manifesto’s current version, 2.0, was published on 16 September 2014 by Jonas Boner, Dave Farley, Roland Kuhn, and Martin Thompson.
This article reads each trait in the manifesto’s own words and turns it into decisions you can check in a design review. The .NET code shows the mechanisms behind the traits: timeouts, a circuit breaker, a bulkhead, a resilient HTTP client, and a Service Bus consumer built for redelivery. I ran the resilience code and only compiled the consumer. The article ends with what reactive design costs and a checklist for reviewing a service.
What the Manifesto Says
The manifesto fits on one page: a few paragraphs on why it was written, one paragraph per trait, and a short close. Each trait’s paragraph opens with the sentence to remember:
- Responsive: “The system responds in a timely manner if at all possible.”
- Resilient: “The system stays responsive in the face of failure.”
- Elastic: “The system stays responsive under varying workload.”
- Message driven: “Reactive Systems rely on asynchronous message-passing to establish a boundary between components that ensures loose coupling, isolation and location transparency.”
The second and third sentences restate the first under a condition: resilience is responsiveness that survives failure, and elasticity is responsiveness that survives changing load. The manifesto’s figure draws the same structure in three rows. Responsive sits in the row labeled value, between Maintainable and Extensible; Elastic and Resilient sit in the row labeled form; and Message Driven sits alone in the row labeled means. Thompson set the means apart in an interview with InfoQ published on 13 October 2014:
The key properties being that a system should be responsive, resilient, and elastic which is fundamentally enabled by being message driven.
The traits had other names at first. Version 1.0, dated 15 July 2013, described “Reactive Applications” that were event-driven, scalable, resilient, and interactive. Version 1.1, dated 23 September 2013, replaced interactive with responsive. Version 2.0, a shorter rewrite, renamed two more: scalable became elastic, and event-driven became message driven. The manifesto’s glossary explains the difference in two sentences: “A message is an item of data that is sent to a specific destination. An event is a signal emitted by a component upon reaching a given state.” A message can carry an event; what it adds is an addressee.
Responsive: Put a Bound on Every Wait
The manifesto’s case for responsiveness has two halves. “Responsiveness is the cornerstone of usability and utility, but more than that, responsiveness means that problems may be detected quickly and dealt with effectively.” The second half is the one that changes code. A call that hangs for 20 seconds hides a problem for 20 seconds. A call that gives up after 200 milliseconds reports it almost at once, and the caller can decide what to do next.
The manifesto asks for “rapid and consistent response times,” with “reliable upper bounds.” Boner and Viktor Klang later put the case plainly in an essay that O’Reilly published on 2 December 2016: “it does not matter if something provides the correct response if the response is not available when it is needed.” A bound is a number you can write down, and each request path needs three decisions about it:
- A deadline for the whole request. Decide how long a caller may wait for the complete answer, retries included, and pass what is left of it to every call the request makes. A latency objective, such as 99 percent of requests within 300 milliseconds, is a separate number: it describes all requests, while the deadline bounds each one.
- A timeout on every remote call, shorter than what is left of the deadline. A call without one can outlast the caller’s whole deadline. Library defaults are rarely the bound you want: in .NET,
HttpClientwaits 100 seconds unless told otherwise. - An answer for when a bound is hit. Return an error the caller can act on, or a reduced answer, as my article on graceful degradation describes.
Measure latency at a high percentile, not as an average. An average hides exactly the slow requests that the bound exists for. Count the requests that run out of deadline, too. My article on observability covers how to measure both.
Resilient: Contain the Failure, Delegate the Recovery
The manifesto names four mechanisms: “Resilience is achieved by replication, containment, isolation and delegation.” Replication keeps another copy ready to answer. Containment and isolation limit how far a failure spreads. Delegation decides who notices the failure and repairs it. One sentence draws the line: “The client of a component is not burdened with handling its failures.” The caller protects itself; repairing the component is someone else’s job.
The glossary also separates failures from errors. A failure is “an unexpected event within a service that prevents it from continuing to function normally.” An error is “an expected and coded-for condition,” such as a rejected input. A 400 response is an answer, not a sign of a sick dependency, so it should not count toward opening a circuit breaker. Polly’s generic breaker counts every exception except a cancellation, including the one EnsureSuccessStatusCode throws for a 400, unless you narrow its ShouldHandle predicate. Microsoft’s HTTP handler, shown below, counts only server errors, 408 and 429 responses, transport exceptions, and timeouts.
Contain It: A Timeout, a Breaker, and a Bulkhead
In .NET, the usual library for containment is Polly. Its version 8 replaced the policies of version 7 with resilience pipelines. The old API still works through the Polly package, but the pipelines have no breaker that counts consecutive failures; Polly’s migration guide says so directly. A version 8 breaker opens on a failure ratio, measured over a sampling window once a minimum number of calls has passed through it. This pipeline guards the pricing call from the opening example. I ran it with Polly 8.8.0 on .NET 10:
using System.Diagnostics;
using Polly;
using Polly.CircuitBreaker;
// The first strategy added is the
// outermost: a bulkhead, then the
// breaker, then a timeout on each
// call, so that a timed-out call
// counts as a failure. Build it
// once and share it.
ResiliencePipeline pricing = new ResiliencePipelineBuilder()
.AddConcurrencyLimiter(
permitLimit: 20, queueLimit: 0)
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 4,
BreakDuration = TimeSpan.FromSeconds(30),
})
.AddTimeout(
TimeSpan.FromMilliseconds(200))
.Build();
for (var call = 1; call <= 6; call++)
{
var clock = Stopwatch.StartNew();
var price = await GetPriceAsync(
pricing, CancellationToken.None);
Console.WriteLine(
$"call {call}: {price} in {clock.ElapsedMilliseconds} ms");
}
// In a request handler, pass the
// request's token: if the caller
// gives up, the call is canceled,
// and the exception propagates.
static async Task<string> GetPriceAsync(
ResiliencePipeline pipeline,
CancellationToken cancel)
{
try
{
return await pipeline.ExecuteAsync(
static async ct => await FetchPriceAsync(ct),
cancel);
}
catch (Exception ex) when (
ex is ExecutionRejectedException
or HttpRequestException)
{
// Timed out, circuit
// open, bulkhead full,
// or a transport error:
// degrade.
return "no price";
}
}
// Stands in for an HTTP call to
// a pricing service that has
// started to hang. It must honor
// the token: Polly's timeout
// only cancels it.
static async Task<string> FetchPriceAsync(
CancellationToken ct)
{
await Task.Delay(
TimeSpan.FromSeconds(20), ct);
return "19.99";
}
Against a pricing service that hangs, it prints:
call 1: no price in 232 ms
call 2: no price in 215 ms
call 3: no price in 200 ms
call 4: no price in 216 ms
call 5: no price in 0 ms
call 6: no price in 0 ms
Each of the first four calls gave up after its 200 milliseconds. Four failures in four calls reached the minimum throughput at a failure ratio of 100 percent, so the breaker opened, and the next calls returned at once without touching the dependency. After the 30-second break, the breaker lets a trial call through and closes if it succeeds; a test with a 1-second break showed exactly that.
Check the breaker’s numbers against your traffic. Polly’s defaults, a 10 percent failure ratio over 30 seconds with a MinimumThroughput of 100, suit a busy client. The documentation warns that below the minimum throughput, “the circuit will not break even if all of the executions failed.” A client quieter than that never trips a default breaker. With the default options, I sent eight calls into the hanging service, and every one of them waited out its full timeout.
The timeout worked because the dependency honors its token. Polly’s timeout cancels the token and waits for the call to stop, and its documentation warns, “It is crucial that the user’s callback respects the cancellation token.” In my test, a call that ignored the token ran its full second through the 200-millisecond timeout and returned normally. GetPriceAsync also takes the caller’s token: when the caller gives up, the call is canceled, and the cancellation propagates instead of turning into a reduced answer.
The bulkhead is the limiter at the top. It lets at most twenty calls through the pipeline at once and refuses the rest immediately, so a hanging dependency can hold twenty of the caller’s slots and no more. That holds only while every call shares this one pipeline; each replica of the service has its own. In a test with thirty simultaneous calls, twenty waited out their timeout and ten were refused within 2 milliseconds.
All three strategies throw a subclass of ExecutionRejectedException, so one catch turns a timeout, an open circuit, or a full bulkhead into the same reduced answer, along with the transport errors the call itself throws.
For HTTP, Start from the Standard Handler
For calls over HTTP, Microsoft packages a pipeline of the same kind. The Microsoft package adds it to a client in one call:
using Microsoft.Extensions.Http.Resilience;
builder.Services
.AddHttpClient<PriceClient>()
.AddStandardResilienceHandler(
options => options.Retry
.DisableForUnsafeHttpMethods());
Microsoft’s documentation lists the five strategies it stacks, from the outermost in:
- a limit of 1,000 concurrent requests;
- a 30-second total timeout;
- three retries with exponential backoff and jitter;
- a circuit breaker with Polly’s default numbers;
- a 10-second timeout on each attempt.
Version 10.11.0 reported the same values at run time. Against a stub that never answered, a GET gave up after 30 seconds, three attempts in.
Those timeouts cover the call only until the response headers arrive. The handler also sets HttpClient.Timeout to infinite, so that its own strategies decide, and no timeout is left to bound the body. When my stub sent its headers at once and then stalled for 150 seconds, the GET waited all 150. Pass the caller’s deadline as a cancellation token: with one set to cancel after 5 seconds, the same read gave up at 5.
Two defaults deserve a second look. Thirty seconds is a long time to keep a person waiting, so set the total timeout from your own deadline.
The handler also retries every HTTP method, and the documentation warns that retrying a POST that inserts a record “could lead to data duplication.” DisableForUnsafeHttpMethods turns retries off for POST, PATCH, PUT, DELETE, and CONNECT; with it, the same stub cost a POST one attempt and 10 seconds. The list is conservative: RFC 9110 defines PUT and DELETE as idempotent, so retrying them does no harm when the server implements them that way. A POST needs an idempotency key that the server enforces, as my article on idempotency in APIs shows.
Delegate the Recovery
A timeout and a breaker protect the caller. They do not repair the dependency. The manifesto gives that job to someone else: “Recovery of each component is delegated to another (external) component.” Erlang builds this in: a supervisor’s job, its documentation says, “is to keep its child processes alive by restarting them when necessary.” Akka.NET brings supervision to .NET. There, a failing actor suspends itself “and sends a message to its supervisor, signaling failure”: the manifesto’s failures as messages, at the scale of a single actor.
Many systems also delegate recovery to the platform. Kubernetes restarts a container whose liveness probe keeps failing; its documentation says, “Liveness probes determine when to restart a container.” A broker redelivers a message whose handler crashed and, after enough attempts, sets it aside, as the section on messages shows. Delegation can misfire, though. Kubernetes warns that “Incorrect implementation of liveness probes can lead to cascading failures,” restarting containers under high load and pushing more work onto the pods that remain. Its own example of what a liveness probe should catch is a deadlock: a state that a restart actually fixes.
Replication is the plainest of the four mechanisms. Run at least two instances, placed so that one fault cannot take both, and a restart or a lost machine still leaves someone to answer. The manifesto asks for replication “where necessary”; for anything a customer touches, it usually is.
Elastic: Scale Out, and Back In
Elasticity means that the system “can react to changes in the input rate by increasing or decreasing the resources allocated to service these inputs.” The glossary separates it from scalability: “Elasticity therefore builds upon scalability and expands on it by adding the notion of automatic resource management.” A scalable system can use more resources. An elastic one adds them, and removes them again, without a person in the loop.
Removing them was the point of the rename. In Thompson’s words, “Most people consider scalable to mean increasing scale. It can be just as important to scale back so a business can save costs.” He noted that scaling in also tests resilience: “Removing a component due to failure or scaling back is effectively the same thing.” An instance that cannot be removed safely during a quiet hour cannot be lost safely during a busy one.
The manifesto states the design condition as well: “no contention points or central bottlenecks,” so that components can be sharded or replicated. In practice:
- Keep instances stateless, or partition the state so that each instance owns a share of it. Sessions in memory, files on a local disk, and in-process caches that must agree all make an instance hard to add or remove.
- Scale queue consumers on the backlog, not on CPU alone. A consumer that waits on I/O shows little CPU while its queue grows. Scale it on queue depth or on the age of the oldest message. KEDA’s Azure Service Bus scaler, for example, adds replicas as active messages pile up in a queue, up to the maximum replica count you set.
- Cap the scale-out. Microsoft’s Queue-Based Load Leveling pattern warns that “Autoscaling without bounding consumers’ aggregate downstream rate only moves the overload to downstream dependencies.” Twenty new consumers can overwhelm the database that one consumer was protecting.
- Drain before you remove. An instance that is scaling in should stop taking new work and finish or release what it holds. With Service Bus in its default peek-lock mode, a message that a lost instance never settled becomes available again when its lock expires, so the work is delayed rather than lost.
Elasticity is also slow. A new instance has to start, warm up, and join, while an overload arrives at once. For that gap, the manifesto relies on back-pressure. In the glossary’s words, a component under stress “should communicate the fact that it is under stress to upstream components and so get them to reduce the load.” A queue between tiers buys time too, which is what load leveling is for, but it absorbs bursts, not a sustained overload. My article on the Backpressure-First Principle covers the limits that keep such a queue honest.
Message Driven: The Boundary That Makes It Work
The fourth trait is the means. The asynchronous boundary between components “also provides the means to delegate failures as messages.” Explicit message-passing “enables load management, elasticity, and flow control by shaping and monitoring the message queues in the system and applying back-pressure when necessary.” Delegation, elasticity, and back-pressure all rest on that boundary.
Being message driven does not require a broker. Thompson was explicit: “Any communication mechanism that can provide an asynchronous binary boundary is sufficient.” He named WebSockets and asynchronous protocols over REST as alternatives to a messaging product. What the boundary may not do is pretend that the network is not there. The glossary says that reactive systems “embrace the network and all its constraints,” such as partial failure and dropped messages, instead of hiding them behind calls that only look local. A message can be lost, delayed, or delivered twice, and the code on both sides has to expect it.
With a broker, the boundary comes with concrete rules. This consumer reads orders from an Azure Service Bus queue and hands each one to an inbox, described below. It compiles against Azure 7.21.0, and because I did not run it against a live namespace, the behavior described after it comes from Microsoft’s documentation:
await using var client = new ServiceBusClient(
"shop.servicebus.windows.net",
new DefaultAzureCredential());
await using var processor = client.CreateProcessor(
"orders",
new ServiceBusProcessorOptions
{
// At most eight handlers
// run at once; the rest
// of the backlog waits
// in the queue.
MaxConcurrentCalls = 8,
// Complete only after
// the work commits.
AutoCompleteMessages = false,
});
processor.ProcessMessageAsync += async args =>
{
var order = TryRead(args.Message.Body);
if (order is null)
{
// Redelivery cannot fix
// unreadable JSON, so
// dead-letter it now.
await args.DeadLetterMessageAsync(
args.Message, "UnreadablePayload");
return;
}
// Records the message ID and
// the order's writes in one
// transaction; a duplicate
// changes nothing.
await inbox.HandleOnceAsync(
args.Message.MessageId,
order,
args.CancellationToken);
await args.CompleteMessageAsync(
args.Message);
};
// Reports receive and handler
// errors. The processor itself
// abandons a message whose
// handler threw before settling.
processor.ProcessErrorAsync += args =>
{
logger.LogError(args.Exception,
"Service Bus error in {Source}", args.ErrorSource);
return Task.CompletedTask;
};
await processor.StartProcessingAsync();
// Processing has started. Keep
// the host alive; at shutdown,
// StopProcessingAsync waits for
// the handlers in flight.
static Order? TryRead(BinaryData body)
{
try
{
return body.ToObjectFromJson<Order>();
}
catch (JsonException)
{
return null;
}
}
Five behaviors carry the traits:
- The consumer sets its own pace.
MaxConcurrentCallscaps the handlers that run at once, and it defaults to one. Prefetching is off by default, so the backlog stays in the broker rather than in the consumer’s memory. That bounds the consumer’s own work, but it does not slow the producer; only the broker’s limits, or a signal you add, can do that. - Completion follows the commit. With
AutoCompleteMessagesoff, the handler completes each message after its work is saved. If the handler throws before settling the message, the SDK’s reference documentation says the message “will be automatically abandoned.” - Failures become messages. An abandoned message, or one whose lock expires, goes back to the queue for redelivery. Service Bus counts deliveries, and once a message exceeds the maximum delivery count, ten by default, it moves to the dead-letter queue. An abandon releases the message at once, so during a short database outage, ten fast failures can dead-letter a good order. Raise the count, which Microsoft allows, or let the handler pause before it rethrows. The failed message then waits, with its reason, for something else to deal with it: delegation in a broker’s terms.
- Bad input skips the retries. A payload that cannot be read goes to the dead-letter queue at once, because redelivery cannot fix it. So should one that reads but is not valid:
TryReadturns{}into an order with an empty ID and a zero total. Service Bus never empties a dead-letter queue on its own, so give it an alert on its depth, an owner, and a way to resubmit. - A full queue says no. Every Service Bus queue has a configured maximum size, from 1 GB to 80 GB depending on the tier and on partitioning. When the queue is full, Microsoft’s quota documentation says that “Subsequent incoming messages are rejected, and the calling code receives an exception.” That refusal is the back-pressure that reaches the producer, which needs a plan for it.
Redelivery has a price: a message can arrive more than once. A second copy can even arrive while the first is still being handled. A Service Bus lock lasts 1 minute by default. The processor renews it while the handler runs, for up to 5 minutes by default. If the lock lapses anyway, “the message goes back to the front of the retrieval order for redelivery,” possibly to another consumer.
So the handler must be idempotent, and the obvious version has a gap. Suppose it takes four separate steps: check whether the message was already completed, claim it, do the work, and mark it completed. If the process crashes or the work throws after the claim, the claim stays. Every redelivery then finds the message claimed and returns early, and with automatic completion on, the message is completed without its work ever being done.
The fix is to make the claim and the work one transaction. Record the message ID and the order’s database writes together, so that a failure rolls back both and the redelivery starts clean. That works only if every copy carries the same ID, so the sender should set it from the order’s identifier, as Microsoft’s settlement guide suggests. The inbox in my article on the transactional outbox works this way, even when two copies arrive together. A payment call or an email sits outside that transaction, so it needs its own idempotency key or an outbox.
Reactive Programming Is Not a Reactive System
“Reactive” also names a programming style: Reactive Extensions, stream libraries, and other ways to compose asynchronous events. Boner and Klang wrote their 2016 essay to separate the two. They call reactive programming “a very useful implementation technique, which can be used in a reactive architecture.” Then they mark its limit: “Remember that it will only help manage one part of the story: dataflow management through asynchronous and nonblocking execution—usually only within a single node or service.”
The same holds for async and await. Non-blocking code lets a few threads serve many waiting requests, the benefit the manifesto names when it says, “Non-blocking communication allows recipients to only consume resources while active.” It does not bound a wait, contain a failure, or add an instance. A checkout written asynchronously from top to bottom, with no timeout on the pricing call, still piles up waiting requests. It just piles them up without blocking threads.
A second mistake hides blocking behind async. Wrapping a blocking call in Task.Run makes it look asynchronous while it still holds a thread. Microsoft’s ASP.NET Core best practices say it directly: “Do not use Task.Run to make a synchronous API asynchronous.” My Software Development Principles series gives the manifesto its own entry, and its list of anti-patterns names both mistakes: reactive in name only, and synchronous calls hidden in asynchronous wrappers.
What It Costs
Reactive design pays for good behavior under stress with complexity everywhere else. A message that leaves the process adds serialization and a network hop that a method call does not. The broker becomes a dependency that must be resilient itself. At-least-once delivery forces idempotent handlers. Copies of data kept in step by messages are at best eventually consistent, as my article on eventual consistency explains, and an asynchronous flow is harder to trace and test than a call stack.
So match the traits to the requirement. Thompson called the reactive properties “quality of service requirements” and warned that “they are much harder to retrofit.” An internal tool used during business hours can be a plain request-and-response application with timeouts on its calls. A checkout that must stay up through a sale is where all four traits pay for themselves. For an existing system, Thompson’s advice was to proceed piece by piece: “identify bounded contexts in a system and then isolate them” behind asynchronous boundaries.
A Checklist for Reviewing a Service
- Bounds. Does every request path have a deadline that its calls inherit, and a latency objective measured at a high percentile? Does every remote call have a timeout, and does the code that makes it honor cancellation?
- Containment. Does a failing dependency fail fast, behind a breaker whose minimum throughput your traffic actually reaches? Is each dependency’s share of the caller’s connections and concurrency capped? Do expected errors stay out of the failure count?
- Recovery. What restarts a crashed component, and could it restart healthy ones under load by mistake? Do at least two instances run where one fault cannot take both?
- Elasticity. Can an instance be added or removed without anyone noticing? Do consumers scale on their backlog, within limits that protect what they call?
- Messages. Is every queue bounded, and does the producer handle a refusal? Is every payload validated, and every handler idempotent, with its claim and its work in one transaction? Does a message complete only after its work commits? Can a short outage dead-letter good messages? Does the dead-letter queue have an alert, an owner, and a way to resubmit?
- Cost. For each trait you add, can you name the requirement that pays for it?
Further Reading
- The Reactive Manifesto (Jonas Boner, Dave Farley, Roland Kuhn, and Martin Thompson, 16 September 2014), with its Glossary
- Martin Thompson Discusses the Reactive Manifesto 2.0 (Harry Brumleve, InfoQ, 13 October 2014)
- Reactive Programming vs. Reactive Systems (Jonas Boner and Viktor Klang, O’Reilly, 2 December 2016)
- The Reactive Principles (Jonas Boner and contributors, version 1.0, 15 June 2022)
- Circuit Breaker Resilience Strategy (Polly documentation)
- Build Resilient HTTP Apps: Key Development Patterns (Microsoft Learn, 24 February 2026)
- Message Transfers, Locks, and Settlement (Microsoft Learn, 6 October 2026)