March 3, 20267 min read

I Didn’t Know What Debouncing Was ~ Until Github Made Me Question Reality

TL;DR: Saw a tweet about GitHub not debouncing its code search and thought it was a bug. It’s not. It’s a feature, enabled by a custom, beast-mode search engine called Blackbird written in Rust. This deep dive explains what debouncing is, why it's usually a "best practice," and how GitHub built an infrastructure so powerful that this practice becomes a performance bottleneck. The real lesson: some rules are meant to be broken, but only if you can afford to build a new reality where they don't apply.

Questioning Github

I saw a tweet { https://x.com/Sahil_Gulihar_/status/1951992606818267519 } that broke my brain . It claimed that GitHub’s Code Search sends a network request on every. single. keystroke. Someone commented on that "No way." That's Web Dev 101 stuff. You debounce search inputs. It’s like rule number one for not DDOSing your own backend.

This sent me down a Rabbit Hole , forced me to question the fact that what is Debouncing? the so called best practice . My gut reaction was to assume it's maybe a bug introduced in a recent deploy. It felt wrong, like watching a master chef salt a dish with a firehose.

You see as developers, we are trained to see patterns, and one of the most common patterns in user interface design is the slight, almost imperceptible delay in a search box. You type, you pause, and then the results appear. This is the rhythm of the modern web. GitHub’s search, however, operates at the speed of thought, a continuous stream of interaction that feels both alien and superior. This conflict between established dogma and observed reality is where the most interesting engineering stories live.

It was a clear sign that I wasn't just looking at a weird frontend choice; I was looking at the tip of a very large, very expensive infrastructural iceberg.

Debouncing

So i dug deeper as before understanding why Github breaks the rule , we have ti understand the rule itself..

At its core Debouncing is one of the techniques used in performing Rate limiting

It's a way to control how many times a function gets executed over a period of time. Specifically, it consolidates a rapid series of function calls into a single execution that happens only after a period of inactivity.

Let's use an analogy. Imagine you're texting someone who gets a notification for every single letter you type.

'H...i...<backspace>...H...e...y'

Each keystroke pings their phone. It's annoying, inefficient, and wastes everyone's energy. Debouncing is the equivalent of waiting a second after you've finished typing "Hey there!" before you hit send. The recipient gets one notification with one complete thought. That's it.

Another classic example is

Camera Autofocus → ->

Your phone’s camera hunts for focus when you tap the screen. Without debouncing, every micro-shift of your hand would kick the focus motor into action never settling. Instead it waits until your hand stops wobbling for a split second, then locks focus. One steady picture, not a jittery, endless hunt.

Naive Approach

"We've all been there. You're building a search input in React


*// The code we all write first.
function NaiveSearch() {
  const [searchTerm, setSearchTerm] = useState('');

  const handleInputChange = (e) => {
    const term = e.target.value;
    setSearchTerm(term);
    // On every single keystroke, we hit the API.
    api.fetchResults(term); 
  };

  return <input type="text" onChange={handleInputChange} />;
}*

Now why this is bad. It’s not just inefficient , it’s costly $$$

Each keystroke becomes a separate API call. If a user types 'typescript', you've just fired off 10 requests. Your backend engineer, who is now being paged at 3 AM, will thank you. You're paying for 10 database queries when you only needed one.

t -> GET /api/search?q=t
y -> GET /api/search?q=ty
p -> GET /api/search?q=typ
e -> GET /api/search?q=type
... (and so on)

Debouncing Approach

Enough talking now lets see this in action you might think what’s the issue here just implement setTimeout() as my friend suggested

*function debounce(func, delay) {
  let timer; // A closure to hold the timer ID between calls

  return function(...args) {
    // If the function is called again, clear the previous timer
    clearTimeout(timer);

    // Set a new timer
    timer = setTimeout(() => {
      // Only execute the original function after the delay has passed
      func.apply(this, args);
    }, delay);
  };
}

// The smart way.
function DebouncedSearch() {
  // Memoize the debounced function so it's not recreated on every render
  const debouncedFetch = useCallback(debounce(api.fetchResults, 500), []);

  const handleInputChange = (e) => {
    debouncedFetch(e.target.value);
  };

  return <input type="text" onChange={handleInputChange} />;
}*

Yeah yeah, the vanilla JS debounce function looks simple enough. Now try using the same in React.

The React Trap

TL;DR

React doesn’t play well with "stateful functions defined inside render". Debounce is one of those. React will re-run your function component every render, recreating your debounce timer and breaking your logic.

💡 You must lift the debounce function out of render-time volatility using useMemo, useCallback, or useRef.

React makes you pay the “hooks tax” to do what vanilla JS just lets you do in 5 lines.

Every time a React component renders, everything inside its function body is redefined — yes, even that innocent-looking debouncedFetch.

If you wrote:

*function MyComponent() {
  const debounced = debounce(apiCall, 500);
  return <input onChange={e => debounced(e.target.value)} />;
}*

👆 You might think debounce works here. But nope. Each render:

  • Re-creates a new debounced function,

  • Which creates a new internal timer variable,

  • Which can’t clear the previous one,

  • So clearTimeout(timer) becomes a no-op.

Result? You’ve implemented a fake debounce — it triggers on every change after delay, ignoring previous calls.

Solution

🎯 This is the most stable, idiomatic pattern for using debounce in React.

*import React, { useState, useMemo, useCallback } from 'react';
import { debounce } from 'lodash'; // Using a library for robustness

const SearchComponent = () => {
  const [inputValue, setInputValue] = useState('');

  // The actual function we want to run after the delay.
  const sendRequest = useCallback((query) => {
    console.log(`Searching for: ${query}`);
    // fetch(`/api/search?q=${query}`)...
  },);

  // Memoize the debounced version of our request function.
  // This ensures that debouncedSendRequest is the same function instance
  // across re-renders, preserving its internal timer state.
  const debouncedSendRequest = useMemo(() => {
    return debounce(sendRequest, 500);
  },); // Dependency array is key here.

  const handleChange = (e) => {
    const query = e.target.value;
    setInputValue(query);
    debouncedSendRequest(query);
  };

  return <input type="text" value={inputValue} onChange={handleChange} />;
};*

So here's the big reveal. The punchline to this entire investigation. GitHub doesn't debounce because they don't have to.

This is a fundamental architectural decision : where in the stack do you solve the performance problem? GitHub chose to solve it at the source.. the backend. Rather than applying a bandage on the client.

In the world of elite developer tools, the definition of "performant" has evolved. It's no longer just about minimizing server CPU cycles or network bandwidth. It's about minimizing "human latency"

the total time from user intent to system response. In this new performance equation, a 300ms debounce isn't a feature; it's a bug. GitHub's choice to eliminate it is not an oversight; it's the entire point.

🚪So... what replaced the debounce?

GitHub didn’t just drop debounce for vibes. They built something that made it obsolete: a custom-built, Rust-powered search engine that can handle live code queries at scale without flinching.

A system so fast, it doesn’t need to wait for you to stop typing.

But how does that even work?

How do you search through billions of files with zero lag — and no SQL?

That’s what we’re diving into next.

In Post 2, we dive into why their backend can even afford this. No hand-wavy "it's fast" BS. We’ll break down:

  • How inverted indexes work (real ones, not textbook toys)

  • What ngramming does under the hood

  • Why Elasticsearch couldn’t keep up

It’s not magic. It’s just engineering — the kind that makes you rethink the stack from byte 0.

👇 This post is part of a short 3-part series:

🧵 Teaser: The Tweet That Triggered Everything

✅ Post 1: You're reading it!

⏳ Post 2: Inverted Indexing: The Core Pattern Behind Fast Search (Coming soon)

🔮 Post 3: Inside Blackbird: GitHub’s Rust-Powered Escape from Elasticsearch (Coming soon)

© 2026 Not a Blogger.
Written by .