Ask a coding agent to “design a scalable API” and watch what happens. Nine times out of ten you’ll get Kubernetes, Kafka, Redis and three microservices — for a product with 100 users and one developer.
It’s not that the agent is wrong about what big systems look like. It’s that it learned the aesthetics of system design from conference talks and engineering blogs, drawn almost entirely from the largest 0.1% of systems on Earth. Netflix writes about Netflix’s problems. Nobody writes a viral post titled “We ran a single Postgres instance and everything was fine.”
So the agent never learned the economics. And the economics are brutal in their simplicity:
100 users at 40 requests per session is 0.28 requests per second. A single application instance has roughly four orders of magnitude of headroom against that. There is no architecture decision to make here — there’s an arithmetic problem, and the arithmetic takes ten seconds.
That’s why I built OAB — Open Architecture Brain. It’s an open-source plugin that gives your coding agent architectural judgment, not just architectural vocabulary.
The bit a generic assistant never produces
Plenty of tools will tell you to keep things simple. That’s just a different flavour of vibes. OAB does something more useful: it computes the capacity envelope, refuses what the numbers don’t justify, and — this is the part I care about most — names the exact measurement that would reverse the refusal.
Here’s a real fragment from a design run for a tiny startup (100 users, £50/month, two developers):
cache — At 0.24 peak reads/second there is no measured read pressure to relieve. Revisit when: a single query exceeds 10 requests/second at over 50 ms, or database CPU is sustained above 60% for 3 days.
orchestration-platform — Three times the complexity budget and four times the money budget for a system with four orders of magnitude of headroom on one instance. Revisit when: more than 4 independently deployable services exist and a dedicated operations engineer is on the team.
A rejected component with a revisit threshold isn’t a “no”. It’s a “not yet, and here’s the tripwire”. You can disagree with it, but you have to disagree with a number, which is a far better argument than disagreeing with a mood.
Complexity is arithmetic, not advice
Under the hood, every component costs complexity points, and every team has a budget:
Two developers get 4 points. A managed database costs 1; a self-managed orchestration platform costs 4. Go over budget and the component is rejected by default — an override must name what’s being dropped, or who will operate the excess.
The pricing makes it concrete. At roughly £240 per point per month in engineering attention, self-hosting a database to save £250/month actually costs about £720/month. The managed service is cheaper, and OAB says so with arithmetic rather than preference. It’s a calibrated heuristic, not a law — and the output says that too.
Not anti-complexity. Anti-unjustified complexity.
The obvious failure mode for a tool like this is becoming a machine that says “you don’t need it” to everyone. That tool would be useless — it would just be a different bias.
So the evaluation suite guards both directions. One scenario describes a platform at 50,000 requests per second across three regions, and its assertions require the machinery a small system would be refused: the CDN (against 1.04 PB/month of egress, where 85% offload saves roughly $35,000/month), the event stream, the cache. If OAB refuses Kafka to a system that genuinely needs it, the suite fails.
Two of the five scenarios guard against under-building; three against over-building. There’s even a scenario whose correct answer is “nothing needs to change” and one whose correct answer is “these requirements are inconsistent” — the two answers an eager assistant never gives.
And because a framework can be tuned to produce reassuring prose far more easily than the right structure, assertions run against artifact fields, never against words. Scenarios are perturbed 100× and 0.01× to prove the system responds to magnitude rather than recognising specific numbers.
What it isn’t
It’s not a methodology, and it doesn’t compete with one. Process plugins like superpowers govern how your agent works — TDD, debugging, planning, review. OAB governs one decision inside that flow: what this system actually needs. They compose.
It’s also not finished, and I’d rather say so than let you find out: no MCP server yet, one agent integration, 6 knowledge domains of a planned 18. The roadmap is public, and so is a critique of the project’s own founding brief.
Try it
OAB is free, Apache-2.0, local-first — no hosted service, no accounts, no telemetry.
Then ask /oab:design for something modest and see what it refuses you. If you’d rather look before installing, oab.run walks through the same decision end to end — the brief, the numbers, and what gets rejected.
The highest-value contribution, incidentally, requires no understanding of the codebase at all: architecture knowledge units are templates you copy, fill in and send as a pull request. If you’ve operated a system at any scale, you know something the largest 0.1% never wrote down.
There is a moment in System Design interviews that often gives people that familiar feeling of nervousness.
You are comfortably discussing requirements, APIs, databases and components when the interviewer asks:
How many requests per second does this system need to support?
At that point, several questions appear at once.
How many users should I assume? How many actions does each person perform? Do I need an exact answer? What happens if I make a simple calculation mistake?
This stage is known as Back-of-the-Envelope Estimation.
Despite involving numbers, it is not really a maths test.
It is a way of making your reasoning visible.
You do not need the exact answer
When estimating the scale of a system, we are not trying to predict exactly how many servers the application will use three years from now.
The objective is to understand the order of magnitude of the problem.
There is an enormous difference between designing a system for:
300 requests per second;
30,000 requests per second;
300,000 requests per second.
In the same way, storing 50 GB is a completely different problem from storing 50 PB.
The estimate only needs to be good enough to help answer some important questions:
Is this genuinely a large-scale system?
Will most of the traffic come from reads or writes?
Will storage become a significant challenge?
Will we need a cache?
Could bandwidth become a bottleneck?
Will the data need to be distributed across multiple regions?
Before drawing the first component, the numbers already begin to show where the architecture might become difficult.
The interviewer wants to follow your reasoning
A good estimate can begin with a simple sentence:
I am going to state a few assumptions so that we can estimate the order of magnitude.
That sentence completely changes the conversation.
You are not claiming to know the company’s real numbers. You are simply creating a reasonable scenario to guide the design.
For example:
300 million monthly active users;
50% use the application every day;
each user performs two actions per day;
peak traffic is twice the average traffic.
The interviewer may prefer different assumptions, and that is perfectly fine. They can adjust the numbers while the method remains exactly the same.
Your assumptions do not need to be perfect. They need to be clear.
A simple method for estimating scale
Whenever someone asks you to estimate the scale of a system, you can follow this sequence.
1. Calculate the daily active users
We normally begin with the number of monthly active users, known as Monthly Active Users, or MAU.
DAU = MAU × percentage of users active each day
Suppose we have 300 million MAU and assume that 50% of them use the system every day:
300 million × 50% = 150 million DAU
We now have the number of Daily Active Users, or DAU.
2. Calculate the actions performed each day
Suppose each user performs two actions per day:
150 million × 2 = 300 million actions per day
3. Convert daily actions into QPS
There are 86,400 seconds in a day.
Therefore:
Average QPS = requests per day ÷ 86,400
In our example:
300 million ÷ 86,400 ≈ 3,500 QPS
For an initial estimate, we can also round 86,400 to approximately 100,000, or 10⁵.
This makes the mental arithmetic much easier:
300 million ÷ 100,000 ≈ 3,000 QPS
The result is not exact, but it is within the correct order of magnitude.
In a System Design interview, that is usually far more important than reaching the final digit.
4. Account for peak traffic
Traffic is not distributed evenly throughout the day.
An application may receive more traffic in the evening, after a notification is sent or during an important event.
For this reason, we can apply a Peak Factor, usually between two and three times the average traffic.
Peak QPS = Average QPS × Peak Factor
Using a peak factor of two:
3,500 × 2 = 7,000 peak QPS
We now have an initial understanding of the load the system needs to support.
When the estimate reveals the real problem
Let us add storage to the same example.
Imagine that:
10% of posts contain an image or another type of media;
each media object is approximately 1 MB;
the data will be retained for five years.
The daily storage requirement would be:
150 million users
× 2 posts per day
× 10%
× 1 MB
= 30 TB per day
Over five years:
30 TB × 365 × 5 ≈ 55 PB
Notice what happened.
The system handles approximately 3,500 writes per second, which may appear relatively manageable.
However, it also needs to store approximately 55 petabytes of content.
The estimate has shown that the greatest challenge probably does not lie in the number of requests. The more difficult problem may be storage, replication, distribution and delivery of those files.
We discovered this before choosing a database, cache, queue, programming language or cloud service.
That is the real value of Back-of-the-Envelope Estimation.
The numbers that are actually worth memorising
You do not need to memorise dozens of formulas.
A small set of numbers can solve most estimation problems.
86,400 seconds in a day
This is the number used to convert daily actions into QPS.
As a shortcut:
86,400 ≈ 100,000 ≈ 10⁵
Another useful reference is:
1 million actions per day ≈ 10 QPS
100 million actions per day ≈ 1,000 QPS
1 billion actions per day ≈ 10,000 QPS
The storage ladder
Each step represents approximately one thousand times more data:
KB → MB → GB → TB → PB
Keeping this sequence in mind makes it much easier to convert millions of records into gigabytes, terabytes or petabytes.
The peak factor
Average QPS rarely represents the busiest moment of the system.
As an initial approximation:
Peak QPS ≈ Average QPS × 2 or 3
Explaining the assumption is more important than choosing exactly two or three.
The replication factor
This is one of the easiest terms to forget.
Suppose the system stores three copies of every piece of data:
Physical storage = logical storage × 3
Fifty terabytes of logical data can quickly become 150 terabytes after replication.
Availability requires redundancy. Redundancy means copies. Copies require storage.
Bandwidth
Another simple calculation that is frequently overlooked:
Bandwidth = QPS × payload size
One thousand responses per second with a size of 1 MB each represent:
1,000 × 1 MB = 1 GB per second
In this scenario, the limit may not be the CPU or the database. It may be the network and the cost of transferring the data.
Common estimation mistakes
A few simple habits can prevent most problems during the interview.
Always write down the units
The number 5 means nothing on its own.
Is it 5 KB, 5 MB, 5 GB, 5,000 requests or 5 servers?
A missing unit can turn a correct calculation into a completely incorrect conclusion.
Round without feeling guilty
Turn a calculation such as:
99,987 ÷ 9.1
into:
100,000 ÷ 10
You are producing an estimate, not preparing a company’s annual accounts.
State your assumptions aloud
A sentence like this demonstrates organisation and maturity:
I will assume that 40% of users are active each day and that peak traffic is approximately three times the average.
Even if the numbers change, the method remains valid.
Perform a sanity check
Once you have finished, ask yourself:
Does the result seem reasonable?
Am I talking about hundreds or hundreds of thousands of QPS?
Should the storage be measured in gigabytes or petabytes?
Did I forget the replication factor?
Am I mixing bits and bytes?
Is the system read-heavy or write-heavy?
Does the payload size make sense?
This small pause can reveal mistakes before they affect the rest of the design.
A tool for practising the reasoning
To make this process more visual, I created a small collection of resources for practising System Design estimations.
The interactive estimator begins with the well-known Twitter example and shows every step of the calculation.
You can change:
the number of users;
the percentage of users active each day;
the number of actions performed by each user;
the ratio between reads and writes;
the payload size;
the retention period;
the peak factor;
the replication factor;
the estimated capacity of each server.
Rather than displaying only the final result, the tool shows how each value was calculated.
This is important because the objective is not to memorise a prepared answer. The objective is to understand how assumptions become numbers and how those numbers influence the architecture.
I also created a printable A4 poster containing the main formulas, latency references, availability levels, units and mental arithmetic shortcuts.
The idea is simple: print it, place it near your desk and refer to it regularly until the numbers start to feel familiar.
All the material, including the source code, formulas and a collection of revision exercises, is available in my study repository:
Confidence does not come from memorising every answer
The most uncomfortable part of a System Design interview is often not knowing exactly what answer the interviewer expects.
But that is precisely the point.
In real projects, we rarely begin with every piece of information available. We work with incomplete requirements, assumptions, approximate metrics and constraints that change over time.
A good estimate demonstrates that you can work with that uncertainty.
You do not need to predict the future.
You do not need the exact answer.
You do not need to perform every calculation mentally and in silence.
You need to state your assumptions, keep the units visible, round sensibly and explain what the numbers mean for the architecture.
The next time someone asks:
How many requests per second does this system need to support?
Take a breath.
Write down the assumptions.
Divide by 86,400.
Find the order of magnitude.
Then use the result to discover where the system actually becomes difficult.
The nerves may still appear, but now you have a method that tells you exactly where to begin.
Existe um momento em entrevistas de System Design que costuma causar aquele frio na barriga.
Você está conversando sobre requisitos, APIs, bancos de dados e componentes quando o entrevistador pergunta:
Quantas requisições por segundo esse sistema precisa suportar?
Nesse momento, várias dúvidas aparecem ao mesmo tempo.
Quantos utilizadores devo considerar? Quantas ações cada pessoa realiza? Preciso chegar ao número exato? O que acontece se eu errar uma conta simples?
Essa etapa é conhecida como Back-of-the-Envelope Estimation, que podemos traduzir como uma estimativa rápida ou um cálculo de guardanapo.
Apesar de envolver números, ela não é realmente uma prova de matemática.
É uma forma de tornar o seu raciocínio visível.
Você não precisa acertar o número exato
Quando estimamos a escala de um sistema, não estamos tentando prever exatamente quantos servidores a aplicação utilizará daqui a três anos.
O objetivo é descobrir a ordem de grandeza do problema.
Existe uma diferença enorme entre projetar um sistema para:
300 requisições por segundo;
30 mil requisições por segundo;
300 mil requisições por segundo.
Da mesma forma, armazenar 50 GB é um problema completamente diferente de armazenar 50 PB.
A estimativa precisa ser boa o suficiente para ajudar a responder algumas perguntas:
O sistema realmente terá uma escala elevada?
O maior volume estará nas leituras ou nas escritas?
O armazenamento será um desafio relevante?
Precisaremos de cache?
A largura de banda poderá ser um gargalo?
Será necessário distribuir os dados entre várias regiões?
Antes mesmo de desenhar o primeiro componente, os números já começam a mostrar onde a arquitetura poderá ficar mais difícil.
O entrevistador quer acompanhar o seu raciocínio
Uma boa estimativa pode começar com uma frase simples:
Vou declarar algumas premissas para conseguirmos estimar a ordem de grandeza.
Essa frase muda completamente a conversa.
Você não está dizendo que conhece os números reais da empresa. Está apenas criando um cenário razoável para orientar o design.
Por exemplo:
300 milhões de utilizadores ativos por mês;
50% utilizam a aplicação diariamente;
cada utilizador realiza duas ações por dia;
o tráfego no horário de pico é duas vezes maior do que a média.
Caso o entrevistador prefira outras premissas, ele poderá ajustá-las. O método continuará exatamente o mesmo.
As suas premissas não precisam ser perfeitas. Elas precisam ser claras.
Um método simples para estimar a escala
Sempre que alguém pedir uma estimativa, você pode seguir esta sequência.
1. Calcule os utilizadores ativos por dia
Normalmente começamos com o número de utilizadores ativos por mês, chamado de Monthly Active Users, ou MAU.
DAU = MAU × percentagem de utilizadores ativos diariamente
Se tivermos 300 milhões de MAU e assumirmos que 50% utilizam o sistema todos os dias:
300 milhões × 50% = 150 milhões de DAU
Agora temos o número de Daily Active Users, ou DAU.
2. Calcule as ações realizadas por dia
Se cada utilizador realiza duas ações diariamente:
150 milhões × 2 = 300 milhões de ações por dia
3. Transforme ações diárias em QPS
Um dia possui 86.400 segundos.
Portanto:
QPS médio = requisições por dia ÷ 86.400
No nosso exemplo:
300 milhões ÷ 86.400 ≈ 3.500 QPS
Durante uma estimativa inicial, também podemos arredondar 86.400 para aproximadamente 100.000, ou 10⁵.
Isso facilita bastante o cálculo mental:
300 milhões ÷ 100 mil ≈ 3.000 QPS
O resultado não é exato, mas está na ordem de grandeza correta.
Em uma entrevista de System Design, isso costuma ser muito mais importante do que chegar ao último dígito.
4. Considere o horário de pico
O tráfego de uma aplicação não é distribuído de maneira uniforme durante o dia.
Uma aplicação pode receber mais acessos durante a noite, depois do envio de uma notificação ou durante algum evento importante.
Por isso, podemos aplicar um Peak Factor, normalmente entre duas e três vezes o tráfego médio.
Peak QPS = Average QPS × Peak Factor
Utilizando um fator de pico de duas vezes:
3.500 × 2 = 7.000 QPS no pico
Pronto. Já temos uma primeira noção da carga que o sistema deverá suportar.
Quando a estimativa revela o verdadeiro problema
Vamos adicionar armazenamento ao mesmo exemplo.
Imagine que:
10% das publicações possuem uma imagem ou outro conteúdo multimédia;
cada conteúdo possui aproximadamente 1 MB;
os dados serão armazenados durante cinco anos.
O armazenamento diário seria:
150 milhões de utilizadores
× 2 publicações por dia
× 10%
× 1 MB
= 30 TB por dia
Durante cinco anos:
30 TB × 365 × 5 ≈ 55 PB
Observe o que aconteceu.
O sistema possui aproximadamente 3.500 escritas por segundo, um número que pode parecer relativamente administrável.
Por outro lado, ele precisa armazenar aproximadamente 55 petabytes de conteúdo.
A estimativa acabou de mostrar que o maior desafio provavelmente não está no número de requisições. O problema mais complexo pode estar no armazenamento, na replicação, na distribuição e na entrega desses ficheiros.
Descobrimos isso antes de escolher banco de dados, cache, fila, linguagem de programação ou qualquer serviço de cloud.
Esse é o verdadeiro valor do Back-of-the-Envelope Estimation.
Os números que realmente vale a pena memorizar
Você não precisa decorar dezenas de fórmulas.
Um pequeno conjunto de números resolve a maior parte das estimativas.
86.400 segundos por dia
Esse é o número utilizado para transformar ações diárias em QPS.
Como atalho:
86.400 ≈ 100.000 ≈ 10⁵
Outra referência útil:
1 milhão de ações por dia ≈ 10 QPS
100 milhões de ações por dia ≈ 1.000 QPS
1 bilhão de ações por dia ≈ 10.000 QPS
A escada de armazenamento
Cada passo representa aproximadamente mil vezes mais dados:
KB → MB → GB → TB → PB
Se você mantiver essa sequência na cabeça, ficará muito mais fácil converter milhões de registos em gigabytes, terabytes ou petabytes.
O fator de pico
O QPS médio raramente representa o momento mais movimentado do sistema.
Como aproximação inicial:
Peak QPS ≈ Average QPS × 2 ou 3
Mais importante do que escolher exatamente duas ou três vezes é explicar a premissa utilizada.
O fator de replicação
Este é um dos termos mais fáceis de esquecer.
Se o sistema armazena três cópias de cada dado:
Armazenamento físico = armazenamento lógico × 3
Cinquenta terabytes de dados podem rapidamente transformar-se em 150 terabytes após a replicação.
Disponibilidade exige redundância. Redundância significa cópias. E cópias ocupam espaço.
A largura de banda
Outra conta simples, mas frequentemente esquecida:
Bandwidth = QPS × tamanho do payload
Mil respostas por segundo com 1 MB cada representam:
1.000 × 1 MB = 1 GB por segundo
Nesse cenário, o limite pode não estar na CPU ou no banco de dados. Ele pode estar na rede e no custo de transferência dos dados.
Erros comuns durante a estimativa
Alguns pequenos hábitos evitam grande parte dos problemas durante a entrevista.
Escreva sempre as unidades
O número 5 não significa nada sozinho.
São 5 KB, 5 MB, 5 GB, 5 mil requisições ou 5 servidores?
Uma unidade esquecida pode transformar uma conta correta numa conclusão completamente errada.
Arredonde sem culpa
Transforme uma conta como:
99.987 ÷ 9,1
em:
100.000 ÷ 10
Você está fazendo uma estimativa, não fechando a contabilidade anual de uma empresa.
Diga as premissas em voz alta
Uma frase como esta demonstra organização e maturidade:
Vou assumir que 40% dos utilizadores estão ativos diariamente e que o pico é aproximadamente três vezes maior do que a média.
Mesmo que os números mudem, o método continua válido.
Faça uma verificação de sanidade
Ao terminar, pergunte:
O resultado parece razoável?
Estou falando de centenas ou centenas de milhares de QPS?
O armazenamento deveria estar em gigabytes ou petabytes?
Esqueci o fator de replicação?
Estou misturando bits e bytes?
O sistema possui mais leituras ou escritas?
O tamanho do payload faz sentido?
Essa pequena pausa pode encontrar erros antes que eles afetem o restante do design.
Uma ferramenta para praticar o raciocínio
Para tornar esse processo mais visual, criei um pequeno conjunto de recursos para praticar estimativas de System Design.
O estimador interativo começa com o conhecido exemplo do Twitter e mostra cada etapa do cálculo.
Você pode alterar:
número de utilizadores;
percentagem de utilizadores ativos diariamente;
ações realizadas por utilizador;
proporção entre leituras e escritas;
tamanho do payload;
período de retenção;
fator de pico;
fator de replicação;
capacidade estimada de cada servidor.
Em vez de apresentar apenas o resultado, a ferramenta mostra como cada valor foi calculado.
Essa é uma parte importante do processo, porque o objetivo não é memorizar uma resposta pronta. O objetivo é entender como as premissas se transformam em números e como esses números influenciam a arquitetura.
Também preparei um poster imprimível em formato A4 com as principais fórmulas, referências de latência, níveis de disponibilidade, unidades e atalhos de cálculo mental.
A ideia é imprimir, colocar próximo à secretária e consultar com frequência até que os números comecem a parecer familiares.
Todo o material, incluindo o código-fonte, as fórmulas e um conjunto de exercícios de revisão, está disponível no meu repositório de estudos:
A parte mais desconfortável de uma entrevista de System Design costuma ser não saber exatamente qual resposta o entrevistador espera.
Mas esse é justamente o ponto.
Na vida real, raramente começamos um projeto com todas as informações disponíveis. Trabalhamos com requisitos incompletos, premissas, métricas aproximadas e limites que mudam ao longo do tempo.
Uma boa estimativa demonstra que você consegue trabalhar com essa incerteza.
Você não precisa prever o futuro.
Não precisa acertar o número exato.
Não precisa fazer toda a matemática mentalmente e em silêncio.
Você precisa declarar as suas premissas, manter as unidades visíveis, arredondar com bom senso e explicar o que os números significam para a arquitetura.
Na próxima vez que alguém perguntar:
Quantas requisições por segundo esse sistema precisa suportar?
Respire.
Escreva as premissas.
Divida por 86.400.
Encontre a ordem de grandeza.
Depois, use o resultado para descobrir onde o sistema realmente fica difícil.
O frio na barriga pode até continuar aparecendo, mas agora você terá um método para saber exatamente por onde começar.
Count the requests, define a threshold and reject anything above it.
But what happens when traffic arrives at the exact boundary between two time windows? How should a system handle legitimate bursts? Is perfect accuracy worth the additional memory cost? And how do you prevent race conditions when several servers are updating the same counters concurrently?
These questions quickly turn a seemingly simple component into a genuinely interesting distributed systems problem.
While studying rate limiter design, I decided not to stop at diagrams and theoretical explanations. I implemented the five most commonly discussed rate limiting algorithms and built an interactive simulator that runs all of them against the same traffic stream.
The goal is simple: make their trade-offs visible.
A rate limiter controls how frequently a client, user, device or service can perform an operation within a given period.
For example:
A user may create no more than two posts per second.
An IP address may attempt to log in only five times per minute.
A client may call an expensive third-party API no more than 1,000 times per day.
An entire platform may accept a maximum of 100,000 requests per second.
Rate limiting helps systems:
protect services from abusive or accidental traffic;
prevent downstream services from becoming overloaded;
control infrastructure and third-party API costs;
enforce fair usage across customers;
maintain predictable system behaviour during traffic spikes.
However, there is no single perfect rate limiting algorithm.
Each approach makes a different compromise between accuracy, memory consumption, latency, burst tolerance and implementation complexity.
That is exactly what this project demonstrates.
Five Algorithms, One Traffic Stream
The simulator implements five algorithms:
Token Bucket
Leaking Bucket
Fixed Window Counter
Sliding Window Log
Sliding Window Counter
Instead of testing each algorithm with a different example, the simulator feeds the same traffic into all five.
This makes it possible to see where their behaviour begins to diverge.
A steady stream of requests can make every algorithm appear correct. The differences become clear only when the traffic becomes irregular, bursty or deliberately hostile.
1. Token Bucket
The Token Bucket algorithm maintains a bucket containing a limited number of tokens.
Tokens are added at a configured refill rate, up to the maximum capacity of the bucket. Every request consumes one token.
If a token is available, the request is accepted. If the bucket is empty, the request is rejected.
Why it is useful
Token Bucket allows short bursts while still enforcing a sustainable average request rate.
Imagine that a client has been inactive for several seconds. During that quiet period, its bucket may become full. When the client suddenly sends several requests together, it can use the stored tokens.
This is often desirable for public APIs, where a single page load may legitimately trigger multiple requests at once.
Main trade-off
The algorithm requires two parameters:
bucket capacity;
token refill rate.
Those parameters must be tuned carefully. A bucket that is too large may allow excessive bursts, while a bucket that is too small may reject perfectly valid usage.
For many general-purpose APIs, Token Bucket is a strong default choice.
2. Leaking Bucket
The Leaking Bucket algorithm behaves more like a queue.
Incoming requests are placed into a bucket with a limited capacity. Requests then leave the bucket at a constant, predictable rate.
If the queue is already full, new requests are rejected.
Why it is useful
Leaking Bucket smooths irregular traffic into a stable output rate.
This can be valuable when the downstream system has a hard processing limit, such as:
a legacy platform;
a payment provider with a strict transactions-per-second limit;
a mainframe;
a hardware device;
a slow external integration.
Regardless of how bursty the incoming traffic becomes, the downstream service receives work at a controlled pace.
Main trade-off
Old requests can occupy the queue for a significant amount of time.
A large burst may fill the bucket, forcing newer and potentially more relevant requests to wait or be rejected. For interactive APIs, this delay can produce a poor user experience.
Unlike most rate limiters, Leaking Bucket often defers work rather than immediately refusing it.
3. Fixed Window Counter
Fixed Window Counter divides time into predefined windows.
For example, a rule allowing five requests per minute might create windows such as:
14:00:00 to 14:00:59;
14:01:00 to 14:01:59;
14:02:00 to 14:02:59.
Each window has its own counter. Once the counter reaches the configured limit, additional requests are rejected until the next window begins.
Why it is useful
Fixed Window Counter is:
easy to understand;
inexpensive to store;
straightforward to implement with Redis using operations such as INCR and EXPIRE.
It also works well when the boundary itself has business meaning, such as a daily quota that resets at midnight.
The boundary problem
This is where the simulator becomes particularly useful.
Suppose the limit is five requests per minute.
A client sends five requests at the end of one window and another five immediately after the next window begins.
Both counters remain within their individual limits. However, the system has allowed ten requests within a rolling sixty-second period.
That is twice the intended limit.
The simulator includes a Boundary Attack scenario that reproduces this behaviour visually. It is one of the clearest demonstrations of why an algorithm that appears correct under normal traffic may fail under adversarial traffic.
4. Sliding Window Log
Sliding Window Log solves the fixed window boundary problem by storing the timestamp of every request.
Whenever a new request arrives, the limiter:
removes timestamps outside the current rolling window;
adds the new request timestamp;
counts the remaining timestamps;
accepts or rejects the request based on that count.
Redis sorted sets are particularly well suited to this approach because timestamps can be used as scores.
Why it is useful
Sliding Window Log is highly accurate.
For any rolling window, the number of accepted requests can be kept within the configured threshold.
This makes it suitable for sensitive, lower-volume operations such as:
login attempts;
password resets;
payment initiation;
account recovery;
costly third-party API operations.
Main trade-off
Accuracy has a memory cost.
The limiter must store individual request timestamps. Memory consumption therefore grows with request volume rather than merely with the number of users.
For a high-traffic public endpoint, storing millions of timestamps can become extremely expensive.
Sliding Window Log is often the most accurate algorithm in the comparison, but not necessarily the most practical one at scale.
5. Sliding Window Counter
Sliding Window Counter combines ideas from Fixed Window Counter and Sliding Window Log.
Instead of storing every timestamp, it keeps counters for the current and previous windows.
It then estimates the traffic inside the rolling window by applying a weight to the previous window.
For example, if the current rolling window overlaps 70% of the previous fixed window, 70% of the previous counter contributes to the estimate.
Why it is useful
Sliding Window Counter provides:
smoother behaviour around window boundaries;
significantly lower memory consumption than a timestamp log;
better accuracy than a basic fixed window;
only a small amount of state per client and rule.
Main trade-off
The result is an approximation.
The calculation assumes that requests in the previous window were reasonably evenly distributed. In reality, they may all have arrived during a small burst.
Even so, this algorithm often provides one of the best practical compromises between accuracy and efficiency.
The Simulator: Where the Algorithms Disagree
Reading about the algorithms is useful, but watching them process the same requests makes their trade-offs much easier to understand.
The simulator includes traffic patterns designed to expose different behaviours.
Boundary Attack
Requests are placed immediately before and after a fixed window boundary.
The Fixed Window Counter may allow twice the intended rolling-window limit, while the sliding approaches behave differently.
Flash Sale Burst
A quiet service suddenly receives a large spike in requests.
Token Bucket can use tokens accumulated during the quiet period, while stricter algorithms may reject much of the burst.
Leaking Bucket Starvation
A burst fills the queue with older requests.
Newer traffic must wait, sometimes for a considerable period, or is rejected because the queue remains full.
Steady Traffic
Requests arrive at a consistent rate below the limit.
Almost every algorithm performs well.
This scenario teaches an important lesson: the happy path is not enough to evaluate a rate limiter.
Poisson Traffic
Requests have the same average rate as a steady client, but arrive in a more realistic, irregular pattern.
The algorithms begin to disagree even though the average traffic remains below the configured quota.
This reveals the difference between controlling an average rate and enforcing a hard limit within every possible rolling window.
A Deterministic Virtual Clock
The simulator does not depend on real time or use artificial delays.
Time is represented by a deterministic virtual clock. Each algorithm receives a timestamp and decides whether the request should be accepted.
As a result:
a two-minute traffic trace can run almost instantly;
every scenario produces the same result on every execution;
automated tests can reproduce specific examples precisely;
internal algorithm state can be inspected request by request;
race conditions can be demonstrated as controlled event interleavings.
This was an important implementation decision.
A learning simulator must be repeatable. If the output changes because of operating-system scheduling or real-time delays, it becomes far more difficult to understand why an algorithm behaved in a particular way.
The Implementation
The project includes both a command-line simulator and an interactive browser version.
The core simulator is implemented in TypeScript, with each algorithm kept in its own small and heavily documented file.
The repository also contains:
deterministic traffic generators;
named experiment scenarios;
request-by-request tracing;
ASCII timeline rendering;
rolling-window peak calculations;
a distributed race-condition demonstration;
automated tests reproducing the documented examples;
reference Redis Lua scripts;
architecture and distributed-system diagrams;
a browser-based visual simulator.
The command-line version can be used without installing external dependencies:
npm run sim -- --list
npm run sim -- --scenario=fixed-window-edge-burst
npm run sim -- --scenario=flash-sale-burst --trace
npm run sim -- --all
npm run sim -- --race
npm test
The visual version runs as a self-contained browser application and allows the parameters and traffic patterns to be changed interactively.
The Distributed Race Condition
A rate limiter can still be logically correct and fail under concurrency.
Consider a naive Redis implementation:
GET counter
check counter against limit
SET counter + 1
Two or more requests may read the same counter value before any of them writes the updated value.
For example:
Request A reads 3
Request B reads 3
Request A writes 4
Request B writes 4
The final counter is four, even though two requests were accepted. It should have been five.
Under heavy concurrency, this can allow a significant number of requests through the limiter without the counter ever reflecting what happened.
The project includes a deterministic race-condition simulation comparing:
a naive read-check-write implementation;
an atomic check-and-increment operation.
The correct solution is not to place a distributed lock around every request. That would protect correctness by damaging latency and throughput.
Instead, the decision should be executed atomically inside the data store, commonly through:
a Redis Lua script;
atomic Redis commands;
Redis sorted-set operations.
The repository includes production-shaped Lua reference implementations showing how these operations can be combined into a single atomic round trip.
From a Single Server to a Distributed Rate Limiter
A local in-memory counter may work while an application has only one instance.
Once multiple rate limiter instances exist, requests from the same client may reach different servers. If every instance maintains its own counters, none of them has a complete view of the client’s activity.
Sticky sessions might appear to solve this by repeatedly routing a client to the same instance, but they reduce flexibility and complicate scaling.
A more robust design keeps the rate limiter instances stateless and stores shared counters in a centralised system such as Redis.
This introduces further design decisions:
How should Redis keys be sharded?
How should hot keys be handled?
Should the limiter fail open or fail closed when Redis is unavailable?
How much latency can the limiter add to every request?
How should counters be synchronised across regions?
Is approximate global enforcement acceptable?
Should rejected requests be dropped or queued for later processing?
These are the questions that transform a coding exercise into a system design problem.
What I Learnt from Building It
The biggest lesson was that rate limiting is not simply about counting requests.
It is about selecting the behaviour that matches the system being protected.
Token Bucket is a strong choice when legitimate bursts should be accepted.
Leaking Bucket is useful when downstream traffic must remain smooth and predictable.
Fixed Window Counter is simple and efficient, but vulnerable at window boundaries.
Sliding Window Log provides excellent accuracy at a considerable memory cost.
Sliding Window Counter offers a practical compromise between precision and efficiency.
There is no universal winner.
The right choice depends on the endpoint, traffic shape, business requirements, expected scale and consequences of allowing or rejecting too many requests.
A mature platform may use several algorithms simultaneously for different operations.
Explore the Project
This project is part of my ongoing System Design study repository, where I am turning architectural concepts into executable experiments rather than keeping them only as diagrams and notes.
For the best learning experience:
Choose one of the simulator scenarios.
Predict how each algorithm will behave.
Run the simulation.
Compare the accepted requests, rejected requests and rolling-window peak.
Inspect the trace and algorithm state.
Change the parameters and repeat the experiment.
The cases where your prediction is wrong are usually the most valuable ones.
If you are studying System Design, preparing for technical interviews or simply interested in distributed systems, feel free to explore the repository, experiment with the scenarios and contribute your own ideas.
And if you find the project useful, consider giving the repository a star on GitHub. ⭐
Building a platform capable of processing 40 billion requests per month might initially sound like a challenge reserved for companies such as Google, Amazon or Netflix.
However, once we convert that number into requests per second and divide the responsibilities correctly between caching, APIs, messaging systems, databases and asynchronous processing, the problem becomes much easier to understand.
In this article, I will explain how I would design a distributed architecture using Go, Apache Kafka, Redis, Kubernetes, distributed databases and modern observability tools to support this volume reliably and cost-effectively.
This is, of course, a reference architecture. The final implementation would depend on several factors, including:
The type of requests being processed
The average payload and response sizes
The balance between reads and writes
Consistency requirements
Geographical distribution of users
Data residency requirements
Expected availability and latency targets
Converting 40 Billion Requests into Requests per Second
Before selecting technologies, we need to understand the actual traffic volume.
Assuming a 30-day month:
40,000,000,000 requests per month
÷ 30 days
÷ 24 hours
÷ 60 minutes
÷ 60 seconds
≈ 15,432 requests per second
This gives us the following approximate traffic profile:
Period
Approximate volume
Per month
40 billion
Per day
1.33 billion
Per hour
55.5 million
Per minute
925,000
Per second
15,432
An average of approximately 15,000 requests per second is not, by itself, an extreme workload for a modern distributed platform.
The real challenge is handling traffic peaks.
Traffic is rarely distributed evenly throughout the day. Marketing campaigns, notifications, scheduled integrations or external events can multiply the normal traffic volume within seconds.
I would therefore design the platform to handle between five and ten times the average traffic:
Average traffic: approximately 15,000 RPS
Expected peak: approximately 75,000 RPS
Extreme peak: approximately 150,000 RPS
The objective would not be to keep enough infrastructure running permanently for 150,000 requests per second. Instead, the platform should be able to scale towards that volume quickly and safely.
High-Level Architecture
The architecture would be divided into several layers:
Clients and integrations
│
▼
Anycast DNS
│
▼
CDN, WAF and DDoS protection
│
▼
Global load balancer
│
├── Europe region
├── North America region
└── Asia-Pacific region
│
▼
Regional load balancer or Kubernetes ingress
│
▼
Go API services
│
├── Redis Cluster
├── Transactional databases
├── Distributed databases
└── Apache Kafka
│
▼
Asynchronous Go consumers
│
├── Notifications
├── Search indexing
├── Analytics
├── External integrations
└── Object storage
The main request flow would be:
Client
→ CDN and WAF
→ Global load balancer
→ Nearest healthy region
→ API gateway or ingress
→ Go services
→ Redis, database or Kafka
→ Asynchronous processing
1. CDN, WAF and DDoS Protection
The first layer should prevent unnecessary or malicious requests from reaching the internal services.
I would use a platform such as Cloudflare, AWS CloudFront or Fastly to provide:
Content delivery network functionality
Edge caching
DDoS protection
Web Application Firewall protection
Bot detection and mitigation
Rate limiting
TLS termination
Geographical routing
Protection against common web attacks
Whenever possible, public or semi-public responses should be served directly from the edge.
For example, if 30% of all requests could be answered by the CDN, the internal services would avoid processing approximately 12 billion requests per month.
This reduction would directly affect:
Infrastructure costs
CPU consumption
Database utilisation
Application latency
Overall platform stability
The best request for the application infrastructure to process is the request that never reaches it.
2. Multi-Region Architecture
For a global and business-critical platform, I would deploy the system across at least three regions.
For example:
Europe
North America
Asia-Pacific
A global load balancer would direct each user to the nearest healthy region.
The strategy could include:
Active-active regions for APIs
Automatic regional failover
Asynchronous replication for eventually consistent data
A primary region for operations requiring strong consistency
Regional storage for regulated or residency-sensitive data
A multi-region architecture is not only about improving latency. It also protects the system against:
The failure of an entire cloud region
Large-scale networking problems
Cloud provider incidents
Faulty deployments
Operational disasters
3. Cell-Based Architecture
Rather than running every customer inside one enormous shared cluster, I would divide the platform into independent cells.
A request should never wait indefinitely for another service to respond.
Internal Communication
For communication between services, I would consider:
HTTP and JSON for public APIs
gRPC or Connect for internal synchronous communication
Kafka for asynchronous events
Protocol Buffers for high-volume internal contracts
Not every interaction needs to go through Kafka.
Operations requiring an immediate response can use HTTP or gRPC. Kafka should be used when processing can happen asynchronously or when multiple services need to react to the same event.
5. Kubernetes and Autoscaling
The services could run on Kubernetes using a managed platform such as Amazon EKS, Google Kubernetes Engine or Azure Kubernetes Service.
Each service should have:
Multiple replicas
Pod Disruption Budgets
Readiness probes
Liveness probes
Resource requests
Resource limits
Topology spread constraints
Anti-affinity across availability zones
Automatic horizontal scaling
I would not scale services using CPU consumption alone.
Autoscaling decisions should consider:
CPU utilisation
Memory consumption
Requests per second
Active request concurrency
p95 and p99 latency
Internal queue sizes
Kafka consumer lag
Open connections
For example, an API service could scale when:
CPU utilisation exceeds 65%
or
p95 latency exceeds 200 ms
or
active requests per pod exceed 500
For Kafka consumers, consumer lag would be one of the most important scaling signals.
6. Redis for Caching and Temporary Data
Redis would reduce pressure on the databases and provide fast access to short-lived data.
Potential use cases include:
Response caching
User sessions
Rate limiting
Distributed locks
Idempotency keys
Feature flags
Counters
Temporary state
Frequently accessed query results
The target should be a high cache-hit ratio.
For example:
Cache-hit ratio: 90%
Requests reaching the database: 10%
If the platform receives 15,000 requests per second and Redis serves 90% of the required data, the database could receive approximately 1,500 queries per second instead of 15,000.
Redis should not automatically be treated as the permanent source of truth.
The services should also support a controlled degradation mode if the cache becomes unavailable.
7. Database Strategy
There is no single database that is ideal for every type of workload.
I would use different database technologies for different responsibilities.
PostgreSQL
PostgreSQL would be suitable for:
Relational data
Payments
Configuration
Accounts
Permissions
Transactional operations
Data requiring strong consistency
Depending on the volume, the PostgreSQL layer could use:
Table partitioning
Read replicas
PgBouncer
Tenant-based sharding
Independent databases for each cell
A managed solution such as Amazon Aurora PostgreSQL
Distributed Key-Value or Wide-Column Database
For very high-volume data accessed through predictable keys, I would consider:
Amazon DynamoDB
ScyllaDB
Apache Cassandra
Google Cloud Bigtable
These technologies can be suitable for:
Device state
Activity timelines
Counters
Large event histories
Write-heavy workloads
Predictable key-based queries
ClickHouse
For analytics and queries across large event datasets, I would use ClickHouse.
Possible workloads include:
Business reports
Usage metrics
Behavioural analysis
Large aggregations
Operational dashboards
Audit analysis
Running large analytical queries directly against the transactional database would be a mistake.
The database responsible for the product’s day-to-day operations should not also act as its data warehouse.
Object Storage
Historical data, exports and raw events could be stored in:
Amazon S3
Google Cloud Storage
Azure Blob Storage
MinIO
Object storage is considerably cheaper than transactional storage and can later support data reprocessing, analytics or machine-learning workloads.
8. Kafka as the Event Backbone
Kafka would become the backbone of the asynchronous processing layer.
When an important operation occurs, the responsible service would publish an event.
A problematic message should not be retried indefinitely inside the main topic.
Otherwise, one invalid or unprocessable message could block or degrade an entire partition.
13. Backpressure and Load Shedding
When demand temporarily exceeds capacity, the platform needs to protect itself.
This can be achieved through:
Bounded queues
Concurrency limits
Rate limiting
Circuit breakers
Early rejection
Degraded responses
Prioritisation of critical operations
It is often better to reject a small percentage of requests quickly than to allow every service to fail slowly.
Operations could be classified by priority:
Priority 1: authentication, payments and core operations
Priority 2: standard product data
Priority 3: reports and exports
Priority 4: non-essential background operations
During periods of overload, lower-priority operations could be temporarily restricted.
14. Resilience Between Services
Every external dependency should have:
A timeout
A circuit breaker
Limited retries
Bulkhead isolation
A fallback strategy
Dedicated metrics
Retries should only be used for safe or idempotent operations.
An inappropriate retry strategy can multiply an incident.
For example:
Service A retries three times
Service B retries three times
Service C retries three times
A single original request could produce up to 27 internal attempts.
This is known as retry amplification and can turn a minor slowdown into a complete outage.
15. Observability
A platform operating at this scale must allow the engineering team to answer three questions quickly:
What is happening?
Where is the problem?
Which customer or operation is affected?
I would use OpenTelemetry to standardise:
Metrics
Distributed traces
Logs
Context propagation between services
The observability stack could include:
Prometheus
Grafana
Loki
Tempo
OpenSearch
Datadog
New Relic
Honeycomb
Important API Metrics
Requests per second
p50, p95 and p99 latency
Error rate
Timeout rate
Requests by endpoint
Active connections
Number of goroutines
Memory consumption
Garbage collector pauses
Important Kafka Metrics
Messages produced
Messages consumed
Consumer lag
Throughput by partition
Under-replicated partitions
Average message size
Processing duration
Dead-letter queue volume
Important Database Metrics
Active connections
Queries per second
Slow queries
Lock wait time
Replication lag
Cache-hit ratio
CPU and storage utilisation
Throttling events
Every log entry should contain identifiers such as:
trace_id
request_id
tenant_id
user_id
region
service
version
This would allow an engineer to follow a request from the edge all the way to the final Kafka consumer.
16. Service-Level Objectives and Error Budgets
I would define clear reliability targets.
For example:
Availability: 99.99%
p95 latency: below 200 ms
p99 latency: below 500 ms
Error rate: below 0.1%
An availability target of 99.99% allows approximately 52 minutes of downtime per year.
I would also use error budgets.
If the engineering team consumed the error budget too quickly, feature deployments could be reduced or paused until the platform returned to an acceptable level of reliability.
This makes reliability measurable rather than subjective.
17. Security
Security must exist at every layer of the architecture.
The platform should include:
Web Application Firewall protection
DDoS protection
Rate limiting by IP address, user and tenant
OAuth 2.0 or OpenID Connect
Short-lived access tokens
Key rotation
TLS encryption in transit
Encryption at rest
Centralised secrets management
Least-privilege access policies
Network segmentation
Audit logging
Container vulnerability scanning
A Software Bill of Materials
Signed build artefacts
CI/CD supply-chain protection
Internal services could use mutual TLS or workload identities.
Secrets should never be embedded inside Docker images or stored directly in the source-code repository.
18. Deployment Strategy
A high-volume platform should not deploy a new version directly to 100% of users.
I would use:
Canary deployments
Blue-green deployments
Feature flags
Automatic rollbacks
Progressive delivery
For example:
1% of traffic
5% of traffic
20% of traffic
50% of traffic
100% of traffic
During each stage, the platform should evaluate:
Error rate
Latency
CPU utilisation
Memory consumption
Kafka consumer lag
Business metrics
If a regression is detected, the deployment should be interrupted or rolled back automatically.
19. Load Testing
No architecture should be considered capable of processing 40 billion requests per month simply because the diagram looks correct.
It must be tested.
I would perform:
Load tests
Stress tests
Spike tests
Soak tests
Chaos engineering experiments
Regional failover tests
Kafka broker failure tests
Redis failure tests
Database degradation tests
Deployment rollback tests
Tools such as k6, Vegeta or Gatling could simulate realistic traffic patterns.
The tests should include:
Realistic payloads
Authentication
Different endpoint distributions
Cold caches
Warm caches
Large tenants
Hot keys
Regional traffic
Slow client connections
Partial dependency failures
The objective is not only to discover the maximum number of requests per second.
It is also necessary to understand how the system behaves when it reaches its limits.
A well-designed platform should fail in a predictable and controlled manner.
20. Network Traffic Estimate
The amount of network traffic depends heavily on the average response size.
If each response averages 5 KB:
40 billion × 5 KB
≈ 200 TB of response data per month
If each response averages 20 KB:
40 billion × 20 KB
≈ 800 TB of response data per month
These estimates do not include:
HTTP headers
Retries
Database replication
Internal service communication
Kafka messages
Logs
Distributed traces
Cross-region traffic
This is why compression, CDN caching, regional routing and careful payload design would have a significant effect on the final infrastructure cost.
The Complete Architecture
My reference architecture would include the following components:
Edge
├── Anycast DNS
├── CDN
├── WAF
├── DDoS protection
└── Rate limiting
Routing
├── Global load balancer
├── Regional load balancers
└── API gateway or Kubernetes ingress
Application
├── Services written in Go
├── HTTP and JSON for public APIs
├── gRPC or Connect for internal communication
├── Bounded concurrency
├── Timeouts
├── Circuit breakers
└── Graceful degradation
Asynchronous processing
├── Apache Kafka
├── Schema Registry
├── Transactional Outbox
├── Idempotent consumers
├── Retry topics
└── Dead-letter queues
Data
├── Redis Cluster
├── PostgreSQL
├── Distributed key-value database
├── ClickHouse
├── Search engine
└── Object storage
Infrastructure
├── Kubernetes
├── Multi-region architecture
├── Cell-based architecture
├── Autoscaling
├── Infrastructure as Code
└── Progressive delivery
Observability
├── OpenTelemetry
├── Prometheus
├── Grafana
├── Distributed tracing
├── Centralised logs
└── SLO-based alerting
Is Kafka Actually Necessary?
One important point is that processing 40 billion requests per month does not automatically mean that the platform needs Kafka.
Kafka would make sense if the product required:
Asynchronous processing
Multiple consumers for the same event
Event reprocessing
Integration between many services
Large event volumes
Analytical pipelines
Decoupling between business domains
For a predominantly synchronous and relatively simple API, introducing Kafka could add unnecessary operational complexity.
The architecture should be driven by the business requirements, not simply by the size of the monthly request estimate.
Conclusion
Forty billion requests per month equates to approximately 15,000 requests per second on average.
This volume can be handled by a modern distributed architecture, but not simply by adding more servers.
Scalability would come from the combination of:
Aggressive caching
Asynchronous processing
Data partitioning
Cell-based isolation
Specialised databases
Stateless services
Idempotent operations
Backpressure
Observability
Load testing
Operational automation
Go would provide a strong foundation for efficient and concurrent services. Kafka would decouple processes and help absorb temporary traffic peaks. Redis would reduce pressure on the databases. A multi-region, cell-based architecture would limit failures and support progressive growth.
However, the most important lesson is that 40 billion requests should not be treated simply as one large monthly number.
The system must be designed around:
Traffic peaks
Latency requirements
Consistency requirements
Message and payload sizes
Geographical distribution
External dependencies
Infrastructure costs
Failure behaviour
The most scalable architecture is not necessarily the one that uses the largest number of technologies.
It is the architecture in which every component has a clear responsibility, operational limits are understood and failures have been anticipated before they occur.
The function removes one notification, processes it, and schedules the next iteration asynchronously.
Because the recursive call is not made directly, it may appear that the application is yielding control back to Node.js.
It is not.
The important detail about process.nextTick()
process.nextTick() does not behave like setTimeout() or setImmediate().
When the current JavaScript operation finishes, Node.js processes callbacks placed in the nextTick queue before allowing the event loop to continue through its normal phases.
Those phases include:
timers, where setTimeout() and setInterval() callbacks run;
poll, where Node.js handles network and filesystem I/O;
check, where setImmediate() callbacks run;
close callbacks, where certain resources are closed.
The nextTick queue has particularly high priority.
Node.js drains this queue before continuing with the event loop. Recursive process.nextTick() calls can therefore keep the application inside that queue indefinitely.
Every callback places another callback into the same high-priority queue.
As long as notifications remain, the queue keeps refilling itself.
The event loop does not get a proper opportunity to move on.
What is event loop starvation?
Event loop starvation happens when one source of work continuously occupies the event loop and prevents other ready work from being processed.
The application is not necessarily dead or crashed.
It is still performing work, but it is unfairly prioritising one category of work so heavily that everything else is left waiting.
Imagine a meeting where one person repeatedly says:
“Just one more thing.”
Every time they finish speaking, they immediately introduce another point before anyone else gets a turn.
The meeting is technically progressing, but nobody else is able to contribute.
That is starvation.
In this Node.js example, the queue consumer continues processing notifications, but HTTP requests, WebSocket connections, timers and other callbacks are never given enough time to run.
Is this a Node.js-only problem?
The specific API shown here, process.nextTick(), is Node.js-specific.
It does not exist in normal browser JavaScript.
However, the broader problem of event loop starvation is not limited to Node.js.
Browsers have their own event loop, task queues and microtask queue. Promise callbacks and callbacks scheduled with queueMicrotask() run as microtasks.
After the current JavaScript task completes, the browser drains the microtask queue before moving to the next task. Microtasks are allowed to schedule more microtasks, and those newly created microtasks are also processed before the browser proceeds.
That means a browser can experience a similar starvation problem:
functionkeepRunning(){queueMicrotask(keepRunning);}keepRunning();setTimeout(()=>{
console.log('This may never run');},0);
Each microtask schedules another microtask.
The browser keeps draining the microtask queue and may never reach the timer callback.
The page may also stop responding to user interaction or fail to repaint because rendering generally needs an opportunity between event loop tasks.
The Node.js version
In Node.js, recursive process.nextTick() can starve:
timers;
filesystem and network I/O;
HTTP request handling;
WebSocket connections;
setImmediate() callbacks.
The browser version
In a browser, an endless microtask chain can starve:
setTimeout() and setInterval();
click, keyboard and other UI events;
network-related task callbacks;
animation frames;
screen rendering and visual updates.
So the broader lesson applies to both environments:
High-priority asynchronous work can still block an application if it continuously schedules more high-priority work.
The API is different, but the starvation pattern is very similar.
Does Promise recursion cause the same problem?
Potentially, yes.
Promise callbacks are placed in the microtask queue:
This creates a continuously replenished microtask queue.
In a browser, it can prevent the next task and rendering opportunity.
In Node.js, Promise microtasks can also delay other event loop work, although process.nextTick() has its own Node-specific scheduling behaviour and is processed with especially high priority.
For this reason, neither process.nextTick() nor Promise microtasks should be used as an unbounded queue-processing mechanism.
A useful distinction
JavaScript itself does not define networking, timers, the DOM or process.nextTick().
Those features are provided by the runtime environment.
The same JavaScript language can therefore run under different event loop implementations:
Environment
High-priority mechanism
What may be starved
Node.js
process.nextTick() and microtasks
I/O, timers, HTTP, WebSockets and setImmediate()
Browser
Promise microtasks and queueMicrotask()
timers, user events, rendering and animation frames
So it is more accurate to say:
Event loop starvation is a runtime scheduling problem, not exclusively a Node.js or browser problem.
The example in this article is Node.js-specific because it uses process.nextTick(), but the underlying concept also exists in browsers.
This pattern appears when one value must be combined with another to satisfy a condition.
Examples include:
two values whose sum equals a target;
two values whose difference equals a target;
checking whether a required counterpart exists.
For Two Sum, the complement is:
const complement = target - currentValue;
Instead of searching the entire array, you check the hash map.
4. Grouping elements
Hash maps are also useful when elements can be represented by a shared key.
For example, in Group Anagrams:
"eat" "tea" "ate"
All three words can be transformed into the same sorted key:
"aet"
The hash map can then associate that key with a list of words.
functiongroupAnagrams(words){const groups =newMap();for(const word of words){const key = word.split("").sort().join("");if(!groups.has(key)){
groups.set(key,[]);}
groups.get(key).push(word);}return[...groups.values()];}
5. Prefix sums
Hash maps are frequently combined with prefix sums.
This happens in problems involving:
subarray sums;
counting subarrays;
finding previous cumulative values;
matching a current sum with an earlier sum.
Suppose the current prefix sum is:
currentSum
To find a subarray whose sum is k, we look for:
currentSum - k
If that value has appeared before, then a valid subarray exists.
This is one of the most important intermediate LeetCode patterns.
6. Sliding window problems
Hash maps and sets are also common in sliding window problems.
A typical example is:
Find the length of the longest substring without repeating characters.
The window moves through the string while a set tracks the characters currently inside it.
functionlengthOfLongestSubstring(text){const characters =newSet();let left =0;let longest =0;for(let right =0; right < text.length; right++){while(characters.has(text[right])){
characters.delete(text[left]);
left++;}
characters.add(text[right]);
longest = Math.max(longest, right - left +1);}return longest;}
In this case, the set provides fast membership checks while the window expands and contracts.
A quick checklist
When reading a problem, ask these questions:
Do I need to check whether something has appeared before?
Do I need to count occurrences?
Do I need fast membership checks?
Am I repeatedly searching through an array?
Do I have nested loops because I am comparing every pair?
Can I transform each element into a useful key?
Do I need to associate an element with an index, count or group?
If the answer to any of these is yes, consider using a Map or Set.
Hash map or sorting?
A hash map is not always the best option.
Sorting may be preferable when:
the order of the elements matters;
you need to process values from smallest to largest;
the problem can be solved with two pointers;
you want to reduce additional memory usage;
you need range-based comparisons.
For example, Two Sum can also be solved by sorting the values and using two pointers.
That solution usually takes:
O(n log n)
It may use less additional memory, but preserving the original indices can make the implementation more complicated.
Time versus space
Hash maps often improve execution time by using more memory.
A common transformation is:
Before: Time: O(n²) Space: O(1)
After: Time: O(n) Space: O(n)
This is known as a time-space trade-off.
You are using additional memory to avoid repeated work.
A useful mental shortcut
A helpful rule is:
If your solution repeatedly searches for something that has already been processed, store it in a hash map.
Another useful rule is:
If an inner loop exists only to check whether a value is present, try replacing it with Map.has() or Set.has().
These two questions alone can help identify many common LeetCode solutions.
Final thoughts
Hash maps are not simply data structures to memorise.
They are tools for avoiding repeated work.
The key skill is recognising when information from previous iterations can be stored and reused.
Whenever you see duplicates, frequencies, complements, grouping, previous values, prefix sums or fast existence checks, a hash map should be one of the first options you consider.
With enough practice, recognising these patterns becomes almost automatic.
For years, API developers have often had to make a choice that never felt entirely right:
use GET and place every filter in the URL;
or use POST to send a complex query in the request body.
The first option follows the correct semantics of a read operation, but becomes increasingly awkward as the query grows.
The second solves the size and structure problem, but communicates a different intention to clients, proxies, caches and the wider HTTP infrastructure.
In June 2026, the IETF published RFC 10008 — The HTTP QUERY Method, introducing an official solution for the space between GET and POST.
Yes, HTTP now has a method called:
QUERY
And no, it is not simply POST /search with a more elegant name.
The Problem QUERY Is Trying to Solve
Consider a simple search API:
GET /products?category=laptops&brand=example&minPrice=500&maxPrice=1500
So far, so good.
Now imagine that the search needs to support:
multiple groups of filters;
AND and OR conditions;
date ranges;
sorting by several fields;
pagination;
aggregations;
geographical filters;
dynamically selected fields;
nested rules.
The URL can quickly begin to look like an attempt to write a programming language using only &, %20 and a great deal of optimism.
GET /products?filter=%7B%22and%22%3A%5B%7B%22category%22...
As well as being difficult to read and maintain, very large URLs encounter practical limits in browsers, proxies, servers, gateways, firewalls and other intermediary systems.
The RFC also points out that URLs are more likely to appear in:
The query remains in the request body, just as it would with POST.
However, the method now explicitly communicates that the operation:
is a query;
is safe;
is idempotent;
can be retried automatically;
may have its response cached.
You can think of QUERY as having the body-carrying capabilities of POST, while retaining semantic properties similar to GET.
QUERY Does Not Mean “GET with a Body”
A natural reaction might be:
Why not simply send a body with a GET request?
Because HTTP does not define general semantics for content sent in a GET request.
Some implementations allow it, but clients, servers, proxies, caches and libraries may treat that body inconsistently or ignore it entirely.
QUERY removes that ambiguity.
Its body is not an accidental implementation detail. The content and its Content-Type are part of the query definition.
QUERY Is Safe
In HTTP terms, a safe method is one where the client does not request or expect a change to the state of the resource being queried.
That means a request such as:
QUERY /orders
should not cancel, update or create orders as part of the operation requested by the client.
This does not prevent the server from performing incidental internal actions such as:
writing logs;
collecting metrics;
populating caches;
updating operational statistics;
creating a temporary resource representing the result.
The important point is that the purpose of the request is to retrieve information, not modify the resource.
In that respect, QUERY belongs to the same semantic category as GET, HEAD and OPTIONS.
QUERY Is Idempotent
An idempotent operation can be repeated without producing additional intended effects beyond those caused by the first execution.
This matters particularly when a network failure occurs.
Imagine that a client sends a request but loses the connection before receiving the response. With an idempotent operation, the infrastructure can retry it more safely.
The RFC does not define a universal query language. It defines the HTTP method and allows each resource to determine the formats and query rules it supports.
The Accept-Query Header
The specification also introduces the Accept-Query response header.
It allows a server to advertise which query formats are accepted by a particular resource:
A client can therefore discover that an endpoint supports QUERY and which formats it may send.
A response might look like this:
HTTP/1.1 200 OK
Allow: GET, HEAD, QUERY
Accept-Query: application/json
It is worth noting that Accept-Query uses the syntax defined by HTTP Structured Fields. Although it may look like a simple comma-separated header, it should be parsed according to the Structured Fields rules.
Error Handling
The RFC suggests suitable HTTP status codes for different classes of error.
400 Bad Request
This may be used when:
Content-Type is missing;
the body is malformed;
the content does not match the declared media type.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"title": "Invalid query document",
"detail": "The request body is not valid JSON."
}
415 Unsupported Media Type
This may be used when the resource does not support the submitted query format:
HTTP/1.1 415 Unsupported Media Type
Accept-Query: application/json
422 Unprocessable Content
This is appropriate when the format and syntax are valid, but the query cannot be processed.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"title": "Invalid query",
"detail": "The field 'customerRank' does not exist."
}
406 Not Acceptable
This may be returned when the server cannot produce a response in a format requested by the client through the Accept header.
QUERY Responses Can Be Cached
Responses to QUERY requests may be cached.
However, there is an important difference compared with GET.
With a GET request, the URI is one of the main elements used to construct the cache key.
With QUERY, the cache must also take into account:
Although they use the same URI, they represent different queries and must not incorrectly share the same cached response.
The cache key therefore needs to incorporate the submitted content.
This also makes caching QUERY requests more complex. An intermediary may need to read the complete request body before it can determine the correct cache key.
Cache Key Normalisation
The RFC allows caches to remove semantically irrelevant differences before generating the cache key.
For example, these two JSON documents may represent the same query:
{"status":"active","country":"GB"}
{
"country": "GB",
"status": "active"
}
A cache that properly understands the format could normalise them to improve efficiency.
However, this must be handled very carefully.
If the cache applies a different normalisation model from the server, two distinct queries could be treated as equivalent, causing the wrong response to be returned.
In multi-tenant systems or environments dealing with sensitive information, this type of mistake could become a serious security vulnerability.
Location and Content-Location
One of the more interesting parts of the RFC is the ability for the server to assign URIs to the query or its result.
Content-Location for the Result
The server may provide a URI representing the specific result of the query:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /query-results/abc123
The client could later retrieve that result using:
GET /query-results/abc123
This may be useful for:
expensive reports;
large datasets;
temporary results;
shareable responses;
analytical processing.
Location for an Equivalent Query
The server may also provide a URI representing the query itself:
HTTP/1.1 200 OK
Content-Type: application/json
Location: /queries/active-uk-customers
A later request might then be:
GET /queries/active-uk-customers
In this case, the server is indicating that the URI can repeat or represent the query without requiring the client to resend the original body.
The distinction is subtle but important:
Content-Location may identify the result that was produced;
Location may identify a resource equivalent to the executed query.
Redirects
Redirect behaviour is also defined.
For 301, 302, 307 and 308 responses, the client may repeat the QUERY request at the new location.
The historical behaviour that sometimes converts a POST into a GET following a 301 or 302 must not be applied to QUERY.
A 303 See Other response, on the other hand, indicates that the result may be retrieved with GET:
HTTP/1.1 303 See Other
Location: /reports/abc123
The client would then follow with:
GET /reports/abc123
This provides an interesting solution for queries that generate persistent or precomputed results.
Conditional Requests
The method can also use conditional request headers such as:
If-None-Match: "query-result-v42"
If the result has not changed, the server may respond with:
HTTP/1.1 304 Not Modified
This can reduce the cost of expensive analytical queries or results that change infrequently.
What About Security?
Moving query parameters from the URL into the request body may reduce accidental exposure.
URLs commonly appear in:
access logs;
browser histories;
analytics systems;
monitoring tools;
tracing platforms;
bookmarks;
referrer headers.
However, this does not make the content secret.
The request body may still be recorded by:
API gateways;
proxies;
web application firewalls;
observability platforms;
debugging tools;
backend applications.
HTTPS, authentication, authorisation, data redaction and sensible logging policies remain essential.
Care must also be taken when the server creates a URI representing a query or its result. Sensitive information from the original request body should not simply be copied into that URI.
QUERY and CORS
In browsers, QUERY is not included in the list of CORS-safelisted methods.
A cross-origin request will therefore require a preflight request:
Representing all of this in a URL would be possible, but hardly pleasant.
Using POST /logs/search would work, but it would not communicate generically that the operation is safe and idempotent.
QUERY expresses that intention precisely.
Can I Start Using It Now?
Technically, the method has been defined and officially registered.
In practice, the publication of an RFC does not mean immediate support across the entire ecosystem.
Before adopting QUERY in production, you would need to test at least:
browsers and HTTP clients;
frontend libraries;
backend frameworks;
web servers;
reverse proxies;
load balancers;
CDNs;
web application firewalls;
API gateways;
tracing tools;
caching systems;
SDK generators;
documentation tools;
observability platforms;
CORS policies.
Some tools may reject unknown methods. Others may allow them but fail to recognise their safety, idempotency or caching semantics.
There is also a risk that an intermediary may accept the request but fail to include the body correctly in the cache key.
That would be a far more serious issue than simply returning 405 Method Not Allowed.
For that reason, the first practical uses of QUERY are likely to appear in controlled environments where the client, server and infrastructure are managed by the same organisation.
What About OpenAPI?
Another practical consideration is support from API description tools.
Even if a server accepts QUERY, the surrounding ecosystem must be able to describe it correctly:
OpenAPI documents;
Swagger interfaces;
client generation;
schema validation;
mocks;
testing tools;
specification-driven gateways.
Until these tools support the method consistently, many teams will continue to use POST /search, even where QUERY would be semantically more appropriate.
Standards do not succeed purely because they are technically elegant. They need to be absorbed by the ecosystem.
Will QUERY Replace POST for Search Endpoints?
Probably not immediately.
The POST /search pattern is already deeply established. It is understood by frameworks, gateways, libraries and documentation tools.
QUERY offers better semantics, but adoption will depend on clear practical benefits:
safer automatic retries;
intermediary caching;
clearer expression of intent;
format discovery through Accept-Query;
greater standardisation across APIs.
For many internal APIs, simply replacing POST with QUERY without taking advantage of these properties may offer little immediate value.
On the other hand, public platforms, analytical APIs and sophisticated distributed systems may benefit significantly from the additional semantic clarity.
My View
RFC 10008 addresses a genuine problem.
Developers have been using request bodies for complex queries for years. What was missing was not a way to perform those queries, but a standardised way to communicate their intention to the rest of the HTTP ecosystem.
QUERY does not make something possible that was previously impossible.
It makes explicit something we were already doing ambiguously.
That matters because HTTP is not merely a transport mechanism between a frontend and a backend. Its semantics influence:
retries;
caches;
proxies;
security;
observability;
failure recovery;
interoperability.
When we choose POST /search, we know that the request is only a query. The rest of the infrastructure may not.
With QUERY, that information becomes part of the protocol itself.
It is still too early to know whether the method will be widely adopted or remain limited to specific APIs and platforms. Its success will depend less on the elegance of the RFC and more on support from browsers, frameworks, gateways, caches and documentation tools.
But the proposal makes sense.
After decades of choosing between enormous URLs and a POST that “does not actually change anything”, HTTP finally has an option designed specifically for complex queries.
Now the entire ecosystem only needs to agree to use it.
Durante anos, quem desenvolve APIs precisou tomar uma decisão que nem sempre parece correta:
usar GET e colocar todos os filtros na URL;
ou usar POST para enviar uma consulta complexa no corpo da requisição.
O primeiro segue corretamente a semântica de uma operação de leitura, mas começa a ficar desconfortável quando a consulta cresce.
O segundo resolve o problema do tamanho e da estrutura dos parâmetros, mas comunica uma intenção diferente para clientes, proxies, caches e outras partes da infraestrutura HTTP.
Em junho de 2026, a IETF publicou a RFC 10008 — The HTTP QUERY Method, propondo uma solução oficial para esse espaço entre GET e POST.
Sim, agora temos um método HTTP chamado:
QUERY
E não, ele não é apenas um POST /search com um nome mais elegante.
O problema que o QUERY tenta resolver
Considere uma API de pesquisa simples:
GET /products?category=laptops&brand=example&minPrice=500&maxPrice=1500
Até aqui, tudo bem.
Agora imagine que a pesquisa precisa suportar:
vários grupos de filtros;
condições AND e OR;
intervalos de datas;
ordenação por múltiplos campos;
paginação;
agregações;
filtros geográficos;
campos dinamicamente selecionados;
regras aninhadas.
A URL pode rapidamente começar a parecer uma tentativa de escrever uma linguagem de programação usando apenas &, %20 e muita esperança.
GET /products?filter=%7B%22and%22%3A%5B%7B%22category%22...
Além de difíceis de ler e manter, URLs muito grandes encontram limites práticos em browsers, proxies, servidores, gateways, firewalls e ferramentas intermediárias.
A própria RFC também destaca que URLs são mais propensas a aparecer em:
logs de acesso;
históricos;
bookmarks;
ferramentas de analytics;
sistemas intermediários.
Uma solução bastante comum é trocar o GET por POST:
A consulta continua no corpo da requisição, como aconteceria com POST.
Entretanto, o método agora comunica explicitamente que a operação:
é uma consulta;
é segura;
é idempotente;
pode ser repetida automaticamente;
pode ter sua resposta armazenada em cache.
Podemos pensar no QUERY como uma operação com a capacidade de transportar conteúdo de um POST, mas com propriedades semânticas semelhantes às de um GET.
QUERY não significa “GET com body”
Uma possível reação seria:
Por que não enviar simplesmente um corpo em uma requisição GET?
Porque o HTTP não define uma semântica geral para conteúdo enviado em uma requisição GET.
Algumas implementações até permitem isso, mas clientes, servidores, proxies, caches e bibliotecas podem tratar esse corpo de maneiras inconsistentes ou até ignorá-lo.
O QUERY evita essa ambiguidade.
Seu corpo não é um detalhe acidental. O conteúdo e o respectivo Content-Type fazem parte da definição da consulta.
QUERY é seguro
No contexto HTTP, um método seguro é aquele no qual o cliente não solicita nem espera uma mudança no estado do recurso consultado.
Isso significa que uma chamada como:
QUERY /orders
não deveria cancelar, atualizar ou criar pedidos como parte da operação solicitada pelo cliente.
Isso não impede o servidor de realizar efeitos internos incidentais, como:
registrar logs;
coletar métricas;
preencher caches;
atualizar estatísticas operacionais;
criar um recurso temporário que represente o resultado.
O ponto importante é que o objetivo da requisição é consultar, não modificar o recurso.
Nesse aspecto, QUERY pertence à mesma categoria semântica de GET, HEAD e OPTIONS.
QUERY é idempotente
Uma operação idempotente pode ser repetida sem produzir efeitos pretendidos adicionais além daqueles causados pela primeira execução.
Isso é particularmente importante quando existe uma falha de rede.
Imagine que o cliente enviou uma requisição, mas perdeu a conexão antes de receber a resposta. Com uma operação idempotente, a infraestrutura pode tentar novamente com mais segurança.
Repetir essa consulta não deveria alterar o estado do satélite nem iniciar uma nova operação operacional.
Ela apenas solicita novamente o resultado.
Essa característica permite que clientes, bibliotecas HTTP e componentes intermediários implementem retries automáticos sem o mesmo receio que teriam com um POST.
O Content-Type é obrigatório
Na RFC 10008, o corpo é parte essencial da consulta.
Por isso, o servidor deve rejeitar uma requisição QUERY quando o header Content-Type estiver ausente ou não for consistente com o conteúdo enviado.
Um servidor poderia aceitar outros tipos de consulta:
Content-Type: application/sql
Content-Type: application/jsonpath
Content-Type: application/vnd.example.query+json
A RFC não define uma linguagem universal de pesquisa. Ela define o método HTTP e permite que cada recurso determine os formatos e as regras da consulta que suporta.
O header Accept-Query
A especificação também introduz o header de resposta Accept-Query.
Ele permite que o servidor anuncie os formatos de consulta aceitos por determinado recurso:
Um cliente pode, assim, descobrir que o endpoint suporta QUERY e quais formatos podem ser enviados.
Uma resposta poderia ser:
HTTP/1.1 200 OK
Allow: GET, HEAD, QUERY
Accept-Query: application/json
É importante observar que Accept-Query utiliza a sintaxe de HTTP Structured Fields. Apesar de visualmente lembrar outros headers separados por vírgula, sua interpretação deve seguir as regras definidas para Structured Fields.
Tratamento de erros
A RFC sugere status codes apropriados para diferentes problemas.
400 Bad Request
Pode ser utilizado quando:
o Content-Type está ausente;
o corpo está malformado;
o conteúdo não corresponde ao tipo declarado.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"title": "Invalid query document",
"detail": "The request body is not valid JSON."
}
415 Unsupported Media Type
Pode ser utilizado quando o recurso não suporta o formato enviado:
HTTP/1.1 415 Unsupported Media Type
Accept-Query: application/json
422 Unprocessable Content
É apropriado quando o formato e a sintaxe são válidos, mas a consulta não pode ser processada.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"title": "Invalid query",
"detail": "The field 'customerRank' does not exist."
}
406 Not Acceptable
Pode ser retornado quando o servidor não consegue produzir uma resposta no formato solicitado pelo cliente por meio do header Accept.
QUERY pode usar cache
Respostas a QUERY podem ser armazenadas em cache.
Entretanto, há uma diferença importante em relação a GET.
Em uma requisição GET, a URI é uma das principais partes utilizadas para construir a chave do cache.
No caso de QUERY, o cache também precisa considerar:
Apesar de utilizarem a mesma URI, são consultas diferentes e não podem compartilhar incorretamente a mesma resposta.
A chave de cache precisa incorporar o conteúdo enviado.
Isso também torna o cache de QUERY mais complexo. Um intermediário precisa ler o corpo completo antes de conseguir determinar a chave apropriada.
Normalização da chave de cache
A RFC permite que caches removam diferenças semanticamente irrelevantes antes de gerar a chave.
Por exemplo, estes dois documentos JSON podem representar a mesma consulta:
{"status":"active","country":"GB"}
{
"country": "GB",
"status": "active"
}
Um cache que compreenda corretamente o formato poderia normalizá-los para aumentar a eficiência.
No entanto, isso precisa ser feito com muito cuidado.
Uma normalização diferente daquela utilizada pelo servidor pode fazer duas consultas distintas serem consideradas iguais, resultando no retorno de uma resposta incorreta.
Em sistemas multi-tenant ou que lidam com informações sensíveis, um erro desse tipo pode se transformar em uma vulnerabilidade séria.
Location e Content-Location
Um dos pontos mais interessantes da RFC é a possibilidade de o servidor atribuir URIs à consulta ou ao seu resultado.
Content-Location para o resultado
O servidor pode informar uma URI que representa o resultado específico da consulta:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /query-results/abc123
Posteriormente, o cliente poderia recuperar esse resultado com:
GET /query-results/abc123
Isso pode ser útil para:
relatórios caros;
grandes conjuntos de dados;
resultados temporários;
respostas compartilháveis;
processamento analítico.
Location para a consulta equivalente
O servidor também pode fornecer uma URI que represente a própria consulta:
HTTP/1.1 200 OK
Content-Type: application/json
Location: /queries/active-uk-customers
Uma chamada posterior poderia ser:
GET /queries/active-uk-customers
Nesse caso, o servidor afirma que a URI pode repetir a consulta sem que o cliente precise reenviar o corpo original.
A distinção é sutil, mas importante:
Content-Location pode identificar o resultado produzido;
Location pode identificar um recurso equivalente à consulta executada.
Redirecionamentos
O comportamento de redirecionamentos também foi definido.
Diante de respostas 301, 302, 307 ou 308, o cliente pode repetir uma requisição QUERY no novo destino.
O comportamento histórico que às vezes transforma um POST em GET após um 301 ou 302 não deve ser aplicado ao QUERY.
Já uma resposta 303 See Other indica que o resultado pode ser obtido com um GET:
HTTP/1.1 303 See Other
Location: /reports/abc123
O cliente seguiria com:
GET /reports/abc123
Isso oferece uma solução interessante para consultas que produzem resultados persistentes ou pré-calculados.
Requisições condicionais
O método também pode utilizar headers condicionais, como:
If-None-Match: "query-result-v42"
Caso o resultado não tenha mudado, o servidor pode responder:
HTTP/1.1 304 Not Modified
Isso pode reduzir o custo de consultas analíticas caras ou resultados que mudam com pouca frequência.
E quanto à segurança?
Mover os parâmetros da URL para o corpo pode reduzir a exposição acidental das informações.
URLs frequentemente aparecem em:
access logs;
históricos;
analytics;
ferramentas de monitorização;
sistemas de tracing;
bookmarks;
headers de referência.
Entretanto, isso não torna o conteúdo secreto.
O corpo da requisição ainda pode ser registrado por:
API gateways;
proxies;
WAFs;
ferramentas de observabilidade;
sistemas de debugging;
aplicações backend.
HTTPS, autenticação, autorização, redacção de dados e políticas adequadas de logging continuam sendo indispensáveis.
Também é necessário tomar cuidado quando o servidor cria uma URI para representar uma consulta ou seu resultado. Informações sensíveis do corpo original não devem simplesmente ser copiadas para essa URI.
QUERY e CORS
Nos browsers, QUERY não faz parte da lista de métodos considerados seguros pelo mecanismo de CORS.
Representar tudo isso na URL seria possível, mas dificilmente seria agradável.
Usar POST /logs/search funcionaria, mas perderia a capacidade de comunicar genericamente que a operação é segura e idempotente.
O QUERY expressa precisamente essa intenção.
Posso começar a utilizá-lo agora?
Tecnicamente, o método está definido e registrado.
Na prática, uma RFC publicada não significa suporte imediato em todo o ecossistema.
Antes de adoptar QUERY em produção, seria necessário testar pelo menos:
browsers e clientes HTTP;
bibliotecas frontend;
frameworks backend;
servidores web;
reverse proxies;
load balancers;
CDNs;
WAFs;
API gateways;
ferramentas de tracing;
sistemas de cache;
geradores de SDK;
ferramentas de documentação;
plataformas de observabilidade;
políticas CORS.
Algumas ferramentas podem rejeitar métodos desconhecidos. Outras podem aceitá-los, mas não reconhecer suas propriedades de segurança, idempotência ou cache.
Também existe o risco de um componente intermediário permitir a requisição, mas não incluir corretamente o corpo na chave de cache.
Esse seria um problema muito mais grave do que simplesmente retornar um 405 Method Not Allowed.
Portanto, o primeiro uso do QUERY provavelmente acontecerá em ambientes controlados, nos quais clientes, servidores e infraestrutura são administrados pela mesma equipa.
E o OpenAPI?
Outro ponto prático será o suporte das ferramentas de definição de APIs.
Mesmo que um servidor aceite QUERY, o ecossistema ao redor precisa conseguir descrevê-lo corretamente:
documentos OpenAPI;
interfaces de Swagger;
geração de clientes;
validação de schemas;
mocks;
ferramentas de testes;
gateways baseados em especificações.
Até que essas ferramentas tenham suporte consistente, muitas equipas continuarão usando POST /search, mesmo que QUERY seja semanticamente mais adequado.
Padrões não vencem apenas por serem tecnicamente bons. Eles precisam ser absorvidos pelo ecossistema.
O QUERY substituirá o POST para pesquisas?
Provavelmente não de imediato.
O padrão POST /search já está profundamente estabelecido. É entendido por frameworks, gateways, bibliotecas e ferramentas de documentação.
O QUERY oferece uma semântica melhor, mas a migração depende de benefícios concretos:
retries automáticos mais seguros;
cache intermediário;
melhor descrição da intenção;
descoberta de formatos com Accept-Query;
padronização entre diferentes APIs.
Para muitas APIs internas, apenas trocar POST por QUERY sem aproveitar essas propriedades provavelmente terá pouco retorno.
Por outro lado, em plataformas públicas, APIs analíticas e sistemas distribuídos com infraestrutura sofisticada, essa clareza semântica pode ser bastante valiosa.
Minha visão
A RFC 10008 resolve um problema real.
Desenvolvedores já usam requisições com corpo para consultas complexas há anos. O que faltava não era uma maneira de fazer isso, mas uma maneira padronizada de comunicar a intenção ao restante do ecossistema HTTP.
O QUERY não aparece para tornar possível algo que era impossível.
Ele aparece para tornar explícito algo que já fazíamos de maneira ambígua.
Isso é importante porque HTTP não é apenas um transporte entre frontend e backend. Sua semântica influencia:
retries;
caches;
proxies;
segurança;
observabilidade;
recuperação de falhas;
interoperabilidade.
Ao escolher POST /search, nós sabemos que aquela chamada é apenas uma consulta. O restante da infraestrutura talvez não saiba.
Com QUERY, essa informação passa a fazer parte do protocolo.
Ainda é cedo para dizer se o método será amplamente adoptado ou se ficará restrito a APIs e plataformas específicas. Seu sucesso dependerá menos da elegância da RFC e mais do suporte de browsers, frameworks, gateways, caches e ferramentas de documentação.
Mas a proposta faz sentido.
Depois de décadas escolhendo entre URLs gigantes e um POST que “na verdade não altera nada”, o HTTP finalmente ganhou uma opção criada especificamente para consultas complexas.
Agora só falta o ecossistema inteiro concordar em utilizá-la.
Depois de quase oito anos trabalhando na mesma empresa, estou novamente à procura de uma oportunidade profissional.
Por si só, isso já seria uma mudança significativa. Mas existe um pequeno detalhe: estou voltando ao mercado numa época em que a Inteligência Artificial consegue escrever código, criar testes, explicar sistemas, encontrar bugs e, ocasionalmente, inventar uma biblioteca que nunca existiu com absoluta confiança.
Como Senior Full-Stack Developer, não consigo olhar para esse momento apenas com medo ou entusiasmo. Preciso olhar com experiência.
E é daí que surge uma ideia que tenho chamado de vibe architecture.
Antes veio o vibe coding
O vibe coding popularizou uma maneira diferente de desenvolver software: você descreve o que deseja, conversa com uma IA, aceita algumas sugestões, ajusta outras e, depois de vários prompts, alguma coisa aparece no ecrã.
Às vezes é exatamente o que você pediu.
Às vezes é um sistema de autenticação completo quando você só queria mudar a cor de um botão.
É uma abordagem extremamente poderosa para protótipos, experimentação, automação e validação rápida de ideias. A distância entre imaginar uma funcionalidade e vê-la funcionar tornou-se muito menor.
Mas existe uma diferença importante entre:
“Funcionou no meu computador.”
e:
“Pode colocar em produção, processar pagamentos e atender milhares de utilizadores.”
Normalmente, essa diferença chama-se engenharia de software.
Então, o que seria vibe architecture?
Para mim, vibe architecture é usar a velocidade da IA sem abandonar a responsabilidade técnica.
É permitir que a IA ajude a implementar, investigar, documentar e testar, enquanto o engenheiro continua responsável por perguntas como:
Onde cada responsabilidade deve ficar?
Como os componentes comunicam entre si?
O que acontece quando uma dependência falha?
Como protegemos dados sensíveis?
Como observamos o sistema em produção?
Como a aplicação será mantida daqui a dois anos?
Por que a IA criou seis abstrações para uma função de doze linhas?
Vibe architecture não significa desenhar algumas caixas, adicionar setas e chamar tudo de microsserviço.
Significa transformar intenção em estrutura.
A IA pode gerar uma API rapidamente. Mas alguém ainda precisa decidir se aquela API deve existir, quais limites deve respeitar, como será versionada e o que acontece quando receber cinquenta mil pedidos por minuto.
A IA pode escrever código.
Arquitetura é decidir qual código deveria ser escrito.
A experiência não perdeu valor
Existe uma narrativa recorrente de que a IA tornará desenvolvedores experientes menos necessários.
Eu vejo de outra forma.
Quanto mais código conseguimos produzir, mais importante se torna saber distinguir código útil de código apenas convincente.
Uma pessoa sem experiência pode pedir:
“Crie uma plataforma escalável para milhões de utilizadores.”
Um engenheiro experiente provavelmente perguntará:
“Quantos utilizadores temos hoje?”
Essa segunda pergunta pode economizar seis meses, três microsserviços, dois clusters Kubernetes e várias reuniões sobre custos de cloud.
Depois de anos trabalhando com frontend, backend, APIs, bancos de dados, infraestrutura, integrações, deploys e sistemas em produção, aprendi que o desafio raramente é apenas fazer uma funcionalidade funcionar.
O verdadeiro desafio é fazê-la funcionar:
com segurança;
sob carga;
quando uma dependência está indisponível;
sem destruir funcionalidades existentes;
com logs que realmente ajudem;
e de uma maneira que outro desenvolvedor consiga entender numa segunda-feira de manhã.
IA acelera a implementação. Experiência reduz as decisões erradas.
As duas coisas juntas são muito mais valiosas do que qualquer uma delas isoladamente.
O novo papel do Senior Developer
O Senior Developer da era da IA provavelmente escreverá menos código manualmente em algumas tarefas.
Isso não significa trabalhar menos.
Significa concentrar mais energia em:
compreender o problema real;
especificar requisitos com clareza;
definir limites arquitetónicos;
criar contexto para agentes de IA;
validar decisões técnicas;
revisar código gerado;
construir estratégias de testes;
avaliar segurança e privacidade;
controlar dívida técnica;
e garantir que velocidade não seja confundida com progresso.
Em outras palavras, deixamos de ser apenas autores de código e passamos também a ser orquestradores de sistemas, contexto e agentes.
O prompt torna-se parte da engenharia.
A especificação torna-se mais importante.
A arquitetura torna-se o guardrail.
E o git diff continua sendo obrigatório, porque confiança é importante, mas revisão de código também.
“Mas a IA fez tudo sozinha”
Uma demonstração de cinco minutos pode criar a impressão de que a IA desenvolveu um produto inteiro sozinha.
Normalmente, ela criou:
uma interface bonita;
algumas rotas;
um banco de dados;
autenticação;
e pelo menos uma chave de API exposta no frontend.
Transformar isso num produto confiável exige decisões que não aparecem no vídeo da demonstração.
É necessário pensar em autorização, rate limiting, migrações, recuperação de falhas, auditoria, acessibilidade, monitorização, custos, dependências, licenças, backups e manutenção.
O botão “Generate App” pode gerar a aplicação.
Infelizmente, ainda não existe um botão “Generate Accountability”.
Não quero competir contra a IA
Estar novamente no mercado depois de quase oito anos na mesma empresa naturalmente provoca algumas reflexões.
As ferramentas mudaram. Os processos mudaram. A velocidade mudou.
Mas não acredito que o melhor caminho seja tentar provar que consigo escrever código mais rapidamente do que uma máquina.
Seria como desafiar uma calculadora para uma competição de divisão.
Meu objetivo é mostrar que sei utilizar a IA para entregar software melhor, mais rapidamente e com responsabilidade.
Quero trabalhar em ambientes nos quais a IA não seja tratada nem como mágica, nem como ameaça, mas como uma ferramenta poderosa dentro de um processo de engenharia sólido.
Não quero ser o desenvolvedor que ignora a IA.
Também não quero ser aquele que aceita todo código gerado porque “os testes passaram” — especialmente quando os testes também foram escritos pela mesma IA.
Quero ocupar o espaço entre esses dois extremos.
Os fundamentos continuam vivos
Mesmo com agentes, modelos avançados e desenvolvimento orientado por linguagem natural, algumas coisas continuam surpreendentemente importantes:
requisitos claros;
separação de responsabilidades;
baixo acoplamento;
testes confiáveis;
segurança;
observabilidade;
documentação;
revisão;
simplicidade;
e bom senso.
A IA não elimina esses fundamentos.
Ela aumenta o impacto de aplicá-los — ou de ignorá-los.
Com ferramentas tradicionais, uma decisão arquitetónica ruim poderia levar semanas para se espalhar pelo sistema.
Com agentes de IA, podemos replicá-la em todo o repositório antes do almoço.
Minha próxima fase
Estou a iniciar uma nova etapa profissional como Senior Full-Stack Developer numa era em que desenvolver software está a ser profundamente transformado pela IA.
Levo comigo quase oito anos de contexto, entregas, incidentes, decisões difíceis, integrações, sistemas legados, deploys e aprendizagem contínua.
Também levo curiosidade.
Quero explorar desenvolvimento assistido por IA, agentes, RAG, avaliação de modelos, guardrails, automação e arquiteturas preparadas para sistemas cada vez mais inteligentes.
Mas pretendo fazer isso sem esquecer uma lição básica:
Software não precisa apenas ser gerado. Precisa ser compreendido, operado e mantido.
Talvez esse seja o verdadeiro significado de vibe architecture.
Não é permitir que a IA escolha toda a arquitetura baseada nas vibes.
É criar uma arquitetura tão clara que humanos e agentes consigam trabalhar juntos sem transformar o repositório num escape room.
A IA pode ajudar a construir o futuro do software.