← Back to Blog

Debounce, Throttle, Rate Limit and Friends: 18 Flow-Control Patterns in Simple English

System Design Performance Reliability

Software often gets more work than it should do right now. Too many clicks, too many requests, a slow service, a sudden spike. There is a small set of patterns for this. Each one answers a simple question: how much work should run, and when?

Index

18 patterns in 4 groups. Click any pattern to jump to it.

#PatternThe idea in one line
1. Too Many Events
01DebounceWait until things go quiet.
02ThrottleAt most once every X.
03BatchingCollect, then send together.
04SamplingKeep only some of the events.
05CoalescingSame question? Ask once.
06DeduplicationSeen it before? Ignore it.
2. Too Many Requests
07Rate LimiterYou get N per time window.
08QuotaA long-term budget.
09Concurrency LimitOnly N at the same time.
3. System Under Pressure
10BufferingHold it until the other side is ready.
11QueueingGet in line.
12BackpressureTell the sender to slow down.
13Load SheddingDrop the least important work first.
4. When Things Fail
14TimeoutDo not wait forever.
15Retry with BackoffTry again, but wait longer each time.
16JitterAdd a little randomness.
17Circuit BreakerStop calling a broken service for a while.
18FallbackHave a plan B.
Extra: Cheat Sheet · How They Work Together

Each pattern has a one-line idea, a short explanation, and a diagram. In the diagrams:

incoming event action runs skipped or failed waiting
Group 1 of 4

Too Many Events

The user or the system fires events faster than you need to handle them. These patterns do less work.

Debounce · Throttle · Batching · Sampling · Coalescing · Deduplication

01

Debounce

"Wait until things go quiet."

Every new event restarts a timer. The action runs only after events stop for a set time. If events may never stop (like auto-save while typing), also set a maximum wait, or it will never run.

Use it for: Search-as-you-type, auto-save, window resize.

Debounce timeline: bursts of events, one run after each pauseeventstimerrunstimer resets on each eventquiet → runquiet → run
↑ Back to index
02

Throttle

"At most once every X."

The action runs at most once per time window. Events that arrive in between are skipped. Most tools also run once more at the end of a burst, so the last value (like the final scroll position) is not lost.

Use it for: Scroll and mouse-move handlers, progress updates, game input.

Throttle timeline: many events, one run per 100 ms window100 ms100 ms100 ms100 mseventsrunsone run per window, the rest are skipped
↑ Back to index
03

Batching

"Collect, then send together."

Small items go into a buffer. When it is full, or a timer fires, all items go out in one call.

Use it for: Sending logs and metrics, bulk database inserts, analytics events.

Batching: small items fill a buffer, then go out as one callsmall itemsbatch buffer (size 5)abcdeflush when full or every 1 s1 callserver
↑ Back to index
04

Sampling

"Keep only some of the events."

Handle a small share of events and skip the rest. The share can be fixed (1 in N) or random.

Use it for: Tracing, debug logs, analytics on very busy systems.

Sampling: only every fourth event is kepteventskeptkeep 1 in 4, skip the rest
↑ Back to index
05

Coalescing

"Same question? Ask once."

When several callers ask for the same thing at the same time, make one real call and give every caller the same answer. This avoids a "thundering herd". It is not caching: it shares a call that is still running, while a cache stores finished answers.

Use it for: Cache misses, popular API calls, loading the same file many times.

Coalescing: three identical requests share one database queryuser Auser Buser Cin-flight mapk1 → 1 pending call1 querydatabaseall three get the same answer
↑ Back to index
06

Deduplication

"Seen it before? Ignore it."

Give each request a unique key. Remember keys you have handled, and skip any key you have already seen. Keep keys for a limited time, or the list grows forever.

Use it for: Double-clicked "Pay" buttons, retried webhooks, message queues that deliver twice.

Deduplication: repeated keys are dropped using a seen setrequestsrunsk1k2k1k3k1seen = { k1, k2, k3 }a key seen before is ignored
↑ Back to index
Group 2 of 4

Too Many Requests

Someone asks for more than you want to give. These patterns set a budget.

Rate Limiter · Quota · Concurrency Limit

07

Rate Limiter

"You get N per time window."

Cap how many requests a user, key or IP can make in a time window. Extra requests usually get 429 Too Many Requests, often with a Retry-After header. The diagram shows a token bucket: it refills at a steady rate, and a full bucket lets a short burst through.

Use it for: Public APIs, login attempts, protecting paid third-party services.

Token bucket rate limiter: the bucket holds 5 tokens and has 1 left. Request 1 takes it and is allowed. Request 2 finds the bucket empty and gets a 429. One token is added every second.req 1req 2bucket holds 5, 1 left+1 token every secondreq 1 allowedtakes the last tokenreq 2: 429no token left
↑ Back to index
08

Quota

"A long-term budget."

Like a rate limit, but over a long period, such as a day or a month. Often tied to a plan or a price. When it runs out, APIs usually return 429 or 403.

Use it for: Free vs. paid API plans, storage limits, monthly AI token budgets.

Quota: a per-user counter. 8,200 of 10,000 calls used this month; at 100% requests are blocked until the monthly resetquota[user_42]limit = 10,000used = 8,200resets = monthly8,200 / 10,000 this month82% usedat 100% → blocked until reset
↑ Back to index
09

Concurrency Limit

"Only N at the same time."

Limit how many jobs run at once. New jobs wait for a free slot. Usually built with a semaphore. A bulkhead goes one step further: each dependency gets its own limit, so one slow service cannot use up every slot.

Use it for: Database connection pools, calls to a slow service, CPU-heavy work.

Concurrency limit: at most three jobs run in the pool, others waitwaitingpool: max 3 at oncebusybusyfreedone
↑ Back to index
Group 3 of 4

System Under Pressure

Work arrives faster than you can finish it. These patterns absorb, pace or cut the load.

Buffering · Queueing · Backpressure · Load Shedding

10

Buffering

"Hold it until the other side is ready."

Keep data in memory between a producer (sends data) and a consumer (reads data) that work at different speeds. A buffer smooths out the speed difference. Keep it a fixed size, or it can fill up memory.

Use it for: Video streaming, file reading and writing, network sockets.

Buffering: bursty input is held in memory and read at a steady paceproducerburstybuffernew inold outconsumersteady
↑ Back to index
11

Queueing

"Get in line."

Put jobs in a first-in, first-out (FIFO) list. Workers take jobs from the front at their own speed. A buffer smooths speed; a queue keeps order and hands work to workers, often for later.

Use it for: Sending emails, image processing, background jobs.

Queueing: producers push jobs into a FIFO queue, workers pop themwebapipushj4j3j2j1FIFO queuepopworker 1worker 2first in, first out — workers go at their own speed
↑ Back to index
12

Backpressure

"Tell the sender to slow down."

When the receiver's queue is full, it signals the sender to pause or slow down, so memory does not fill up. Some systems drop or block instead of pausing.

Use it for: Streams, message brokers, data pipelines.

Backpressure: a full queue tells the producer to slow downproducerqueue (max 6): FULLconsumerslow"slow down" signal
↑ Back to index
13

Load Shedding

"Drop the least important work first."

When the system is overloaded, reject low-priority requests before they use up resources, so the important ones still work. This usually returns 503, often with Retry-After, and clients should wait before trying again.

Use it for: Keeping checkout alive during a sale, protecting core APIs during spikes.

Load shedding: under heavy load, low priority work is droppedcheckout · highsearch · highrecs · lowload> 80%?serve highdrop low (503)
↑ Back to index
Group 4 of 4

When Things Fail

Other services will be slow or down sometimes. These patterns fail safely.

Timeout · Retry with Backoff · Jitter · Circuit Breaker · Fallback

14

Timeout

"Do not wait forever."

Set a deadline for every call. If there is no answer in time, give up and return an error. The other side may still finish the work, so be careful when you retry.

Use it for: Every network call, database query and external API.

Timeout: the call is cut at the 2 second deadlinecall2 s deadlinestartnever waited forgive up → return an error
↑ Back to index
15

Retry with Backoff

"Try again, but wait longer each time."

Many errors are short. Retry them, but double the wait after each failure so you do not overload a service that is struggling. Always set a maximum number of tries and a maximum wait. Only retry actions that are safe to repeat (idempotent): reads, or writes with an idempotency key (see Deduplication).

Use it for: Network blips, rate-limited APIs, brief outages.

Retry with exponential backoff: waits of 1, 2 and 4 secondsattempts1 s2 s4 ssuccessfail, fail, faileach wait is twice as long as the last
↑ Back to index
16

Jitter

"Add a little randomness."

If many clients retry on the same schedule, they all hit the server at the same moment. Use a random wait instead of an exact one, for example anywhere between 0 and the backoff time. This spreads them out.

Use it for: Any retry or timer used by many clients at once.

Jitter: retries spread over time instead of hitting togetherno jitterjitterall at once → spikesame retries, spread out
↑ Back to index
17

Circuit Breaker

"Stop calling a broken service for a while."

After too many failures in a row, the breaker opens and calls fail fast without waiting. After a pause it goes half-open: one test call checks if the service is back.

Use it for: Calls to other services and third-party APIs.

Circuit breaker states: closed, open, half-openCLOSEDcalls passOPENfail fastHALF-OPENtest call5 in a rowafter 30 stest failstest ok → back to normal
↑ Back to index
18

Fallback

"Have a plan B."

When the main source fails, answer from a backup: a cache, a default value, or a simpler feature.

Use it for: Showing cached prices, default recommendations, a read-only mode.

Fallback: when the main service fails, use the cacheclient1. trypricing service ✗2. fallbackcache (last good) ✓show pricea slightly old answer beats an error page
↑ Back to index

Cheat Sheet

PatternQuestion it answersWhat happens to extra workWhere you usually add it
DebounceHave they stopped yet?Resets the timerUI / client
ThrottleHas enough time passed?SkippedUI / client
BatchingCan I send these together?Waits for the batchClient or server
SamplingDo I need all of them?SkippedClient or server
CoalescingIs someone already asking this?Shares one resultServer
DeduplicationDid I already do this?IgnoredServer
Rate limiterIs there budget left right now?Rejected (usually 429)Server, at the API edge
QuotaIs there budget left this month?Rejected until resetServer
Concurrency limitIs there a free slot?Waits for a slotServer
BufferingIs the reader ready?Held in memoryAnywhere
QueueingCan this be done later?Waits in lineServer
BackpressureCan the receiver keep up?Sender slows downBetween two services
Load sheddingIs this work important?Dropped (usually 503)Server
TimeoutIs it taking too long?CancelledCaller
Retry + backoffWas it a short error?Tried again, laterCaller
JitterIs everyone retrying together?Spread outCaller
Circuit breakerIs the service broken?Fails fastCaller
FallbackIs there a plan B?Answered from backupCaller

How They Work Together

Real systems mix these patterns. Here is one common way to stack them around a call to an external API, from the outside in:

  1. Fallback: if everything below fails, show the cached answer.
  2. Circuit breaker: if the API keeps failing, stop calling it for a while.
  3. Retry with backoff and jitter: try short errors again, with a maximum number of tries.
  4. Rate limiter: before each try, stay inside the API's limit.
  5. Timeout: never wait more than 2 seconds for one try.

Other orders can work too. The key point is that the breaker wraps the retries, so it sees every failed try.

Start simple. Add a timeout to every network call first. Then add the other patterns where you actually see the problem.