{"id":490255,"date":"2026-08-08T13:15:30","date_gmt":"2026-08-08T13:15:30","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=490255"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=490255","title":{"rendered":"I Spent a Week Debugging Without Google, Stack Overflow, or AI \u2014 What Changed Wasn\u2019t My Speed"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>A few weeks ago I was sitting next to a programmer who has been dealing with production software for longer than I have been using computers.<\/p>\n<p>We were looking at an annoying backend problem. Requests occasionally failed after deployment, but only under load. Restarting the process fixed everything for a while.<\/p>\n<p>My automatic reaction was predictable.<\/p>\n<p>Search the exception. Check Stack Overflow. Ask an AI tool. Search GitHub issues. Maybe paste the suspicious function somewhere and see what comes back.<\/p>\n<p>He did none of that.<\/p>\n<p>For maybe fifteen minutes he barely touched the code.<\/p>\n<p>He checked the logs, wrote three possible causes in a text file, rejected one of them, added a tiny piece of instrumentation, ran the program twice, then opened the implementation of a library function.<\/p>\n<p>At one point I asked whether it would be faster just to search for the error.<\/p>\n<p>His answer was simple: first we need to know what we are searching for.<\/p>\n<p>That sentence annoyed me a little, mostly because it was obviously correct.<\/p>\n<p>A day later I decided to try something stupid: spend one working week debugging without Google, Stack Overflow, Reddit, GitHub issue searches or AI coding assistants.<\/p>\n<p>Not permanently. I am not moving into a cabin with a ThinkPad and a printed POSIX manual.<\/p>\n<p>Just seven days.<\/p>\n<p>The only things allowed were local documentation, official documentation I already knew how to reach directly, source code, tests, logs, Git history and tools already installed on the machine.<\/p>\n<p>The experiment quickly became less about living without search and more about noticing how often I normally skip the actual debugging part.<\/p>\n<h3>The first surprise: I was searching before I had a question<\/h3>\n<p>The first morning was uncomfortable.<\/p>\n<p>A Python service threw an occasional JSON decoding error while reading messages from another process over a socket. Normally the exception text would have been copied into a search engine within thirty seconds.<\/p>\n<p>Instead I opened the code.<\/p>\n<p>The relevant part looked completely harmless:<\/p>\n<pre><code>header = sock.recv(4)size = struct.unpack(\"!I\", header)[0]payload = sock.recv(size)message = json.loads(payload)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>A four-byte length prefix, followed by a JSON payload. Nothing exotic.<\/p>\n<p>The logs pointed at <code>json.loads<\/code>, so the first hypothesis was bad JSON.<\/p>\n<p>I dumped the received bytes.<\/p>\n<p>They were indeed incomplete.<\/p>\n<p>That seemed to confirm the hypothesis for about thirty seconds, until the next question appeared: why would the sender produce incomplete JSON?<\/p>\n<p>The sender used <code>sendall<\/code>. The serialized object was correct before transmission. Checksums matched before the write.<\/p>\n<p>So the JSON parser was probably innocent.<\/p>\n<p>This is where the veteran-programmer habit started making sense. Instead of naming technologies, he had been naming assumptions.<\/p>\n<p>My assumptions were:<\/p>\n<p>The sender produces one complete frame.<\/p>\n<p>The receiver reads four bytes of header.<\/p>\n<p>The receiver then reads exactly the number of payload bytes stored in that header.<\/p>\n<p>The first statement was easy to verify.<\/p>\n<p>The second one was not guaranteed at all.<\/p>\n<p>TCP gives us a byte stream. It does not preserve the boundaries of our application messages. A call to <code>recv(4)<\/code> can return four bytes, but it may also return one, two or three. The same problem applies to the payload.<\/p>\n<p>On a local machine with tiny messages, the bug had hidden itself surprisingly well.<\/p>\n<p>So instead of searching for Python JSON random error, I wrote a small reproducer.<\/p>\n<h4>Python \u2014 deterministic test for fragmented TCP reads<\/h4>\n<pre><code class=\"python\">import jsonimport randomimport socketimport structimport threadingimport timefrom typing import Optionaldef encode_message(data: dict) -&gt; bytes:    payload = json.dumps(        data,        separators=(\",\", \":\"),        ensure_ascii=False,    ).encode(\"utf-8\")    return struct.pack(\"!I\", len(payload)) + payloaddef send_fragmented(    sock: socket.socket,    frame: bytes,    min_chunk: int = 1,    max_chunk: int = 5,) -&gt; None:    \"\"\"    Deliberately split one application frame into many small writes.    TCP does not promise that the receiver will observe the same    boundaries, but this makes partial reads much easier to reproduce.    \"\"\"    offset = 0    while offset &lt; len(frame):        remaining = len(frame) - offset        chunk_size = random.randint(            min_chunk,            min(max_chunk, remaining),        )        chunk = frame[offset:offset + chunk_size]        sock.sendall(chunk)        offset += chunk_size        # Make fragmentation visible even on a fast local machine.        time.sleep(0.002)    sock.shutdown(socket.SHUT_WR)def read_message_buggy(sock: socket.socket) -&gt; Optional[dict]:    \"\"\"    This version contains the assumption that caused the real bug:    one recv() call is expected to fill the requested buffer.    \"\"\"    header = sock.recv(4)    if not header:        return None    if len(header) != 4:        raise RuntimeError(            f\"Partial header: expected 4 bytes, got {len(header)}\"        )    payload_size = struct.unpack(\"!I\", header)[0]    payload = sock.recv(payload_size)    if len(payload) != payload_size:        raise RuntimeError(            f\"Partial payload: expected {payload_size} bytes, \"            f\"got {len(payload)}\"        )    return json.loads(payload)def recv_exactly(sock: socket.socket, size: int) -&gt; bytes:    \"\"\"    Read exactly size bytes unless the peer closes the connection.    \"\"\"    buffer = bytearray()    while len(buffer) &lt; size:        chunk = sock.recv(size - len(buffer))        if not chunk:            raise EOFError(                f\"Connection closed after {len(buffer)} \"                f\"of {size} bytes\"            )        buffer.extend(chunk)    return bytes(buffer)def read_message_fixed(sock: socket.socket) -&gt; Optional[dict]:    first_byte = sock.recv(1)    if not first_byte:        return None    header = first_byte + recv_exactly(sock, 3)    payload_size = struct.unpack(\"!I\", header)[0]    if payload_size &gt; 10 * 1024 * 1024:        raise ValueError(            f\"Refusing suspicious frame size: {payload_size}\"        )    payload = recv_exactly(sock, payload_size)    return json.loads(payload)def run_once(reader) -&gt; None:    sender, receiver = socket.socketpair()    message = {        \"type\": \"build_finished\",        \"project\": \"demo-service\",        \"duration_ms\": 1847,        \"successful\": True,        \"files\": [            \"api.py\",            \"worker.py\",            \"storage.py\",        ],    }    frame = encode_message(message)    thread = threading.Thread(        target=send_fragmented,        args=(sender, frame),        daemon=True,    )    thread.start()    try:        decoded = reader(receiver)        print(\"Decoded:\", decoded)    finally:        receiver.close()        sender.close()        thread.join(timeout=1)def main() -&gt; None:    print(\"Testing buggy reader\")    try:        run_once(read_message_buggy)    except Exception as exc:        print(            f\"{type(exc).__name__}: {exc}\"        )    print()    print(\"Testing fixed reader\")    run_once(read_message_fixed)if __name__ == \"__main__\":    main()<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The important part was not the <code>recv_exactly<\/code> function. That function is boring.<\/p>\n<p>The useful part was finding the false assumption.<\/p>\n<p>Before this experiment, I would probably have searched the exception first. Maybe the correct answer would have appeared immediately. Maybe not.<\/p>\n<p>But the search phrase itself would already contain my interpretation of the bug.<\/p>\n<p>And my interpretation was wrong.<\/p>\n<p>The JSON parser was simply the place where corrupted assumptions finally became visible.<\/p>\n<p>How often does that happen in normal debugging? The line that throws the exception gets accused simply because it happens to be holding the body when the murder is discovered.<\/p>\n<h3>Debugging became much easier when I started writing down what must be true<\/h3>\n<p>By the second day, one habit had changed.<\/p>\n<p>Before touching code, I started writing a tiny list:<\/p>\n<p>Observed fact<br \/>Possible explanation<br \/>Test that could make the explanation false<\/p>\n<p>Nothing fancy.<\/p>\n<p>No elaborate debugging template.<\/p>\n<p>For example, another service was occasionally taking thirty seconds to return even though its normal response time was below a second.<\/p>\n<p>The obvious suspect was the database.<\/p>\n<p>So the first note looked like this:<\/p>\n<p>Observed: HTTP request remains open for about 30 seconds.<\/p>\n<p>Hypothesis: database query blocks.<\/p>\n<p>Test: log timestamps immediately before and after the query.<\/p>\n<p>The query took 14 milliseconds.<\/p>\n<p>Good. One suspect gone.<\/p>\n<p>Next hypothesis: connection pool exhaustion.<\/p>\n<p>Test: record pool acquisition time separately from query execution.<\/p>\n<p>No problem there either.<\/p>\n<p>Next: DNS.<\/p>\n<p>Then TCP connect time.<\/p>\n<p>Then application semaphore.<\/p>\n<p>The difference sounds small, but it changed the whole session. Usually debugging feels like walking through a dark room and touching random objects. Now every command was supposed to kill or strengthen one hypothesis.<\/p>\n<p>That also changed how I used logs.<\/p>\n<p>Previously, adding logs often meant dumping more state.<\/p>\n<p>Now the question became more specific: what is the smallest piece of information that separates hypothesis A from hypothesis B?<\/p>\n<p>That is much more useful than adding ten INFO lines and hoping the bug feels guilty enough to confess.<\/p>\n<p>There was another unexpected effect.<\/p>\n<p>Without a search tab waiting nearby, reading source code stopped feeling like the expensive option.<\/p>\n<p>A library behaved strangely, so I followed the call.<\/p>\n<p>Then another call.<\/p>\n<p>Then one more.<\/p>\n<p>Three minutes later the weird behaviour made complete sense.<\/p>\n<p>This was probably the biggest psychological change during the experiment. Source code had always been available, but search made it feel slower than it actually was.<\/p>\n<h3>The nastiest bug of the week looked like a network problem<\/h3>\n<p>The second interesting failure happened in an asyncio worker.<\/p>\n<p>After several hours, some requests simply stopped progressing. CPU usage was low. Memory looked normal. The database was fine.<\/p>\n<p>Restarting the worker immediately fixed it.<\/p>\n<p>That is exactly the kind of bug where searching feels irresistible.<\/p>\n<p>Asyncio random hang after hours would have been a wonderfully terrible query.<\/p>\n<p>Instead, the process got the same treatment as the socket bug.<\/p>\n<p>First question: what resource do successful requests obtain that stuck requests are waiting for?<\/p>\n<p>The service had a concurrency limit implemented with an <code>asyncio.Semaphore<\/code>.<\/p>\n<p>That immediately became interesting.<\/p>\n<p>The worker allowed three expensive external operations at the same time. Under normal conditions, a task acquired a permit, completed its request and released the permit.<\/p>\n<p>Except sometimes tasks were cancelled.<\/p>\n<p>A timeout higher in the stack could cancel a coroutine while it was suspended inside an <code>await<\/code>.<\/p>\n<p>The release happened after that await.<\/p>\n<p>Which meant it sometimes never happened.<\/p>\n<p>One leaked permit was invisible.<\/p>\n<p>Two made the service slower.<\/p>\n<p>Three turned the semaphore into a tiny permanent parking lot.<\/p>\n<p>The networking code had done nothing wrong.<\/p>\n<p>Again.<\/p>\n<p>I wrote another reduced example.<\/p>\n<h4>Python \u2014 reproducing a leaked asyncio semaphore<\/h4>\n<pre><code class=\"python\">import asynciofrom collections.abc import Awaitable, Callableclass ExternalService:    def __init__(self, concurrency: int = 3) -&gt; None:        self._limit = asyncio.Semaphore(concurrency)    async def call_buggy(self, request_id: int) -&gt; str:        \"\"\"        BUG:        If cancellation happens during the simulated I\/O operation,        release() is never reached.        \"\"\"        await self._limit.acquire()        print(            f\"[buggy] request {request_id} acquired a slot\"        )        await asyncio.sleep(10)        self._limit.release()        return f\"result-{request_id}\"    async def call_fixed(self, request_id: int) -&gt; str:        \"\"\"        The async context manager releases the semaphore        when leaving the block, including cancellation paths.        \"\"\"        async with self._limit:            print(                f\"[fixed] request {request_id} acquired a slot\"            )            await asyncio.sleep(0.1)            return f\"result-{request_id}\"async def cancel_after_acquire(    operation: Callable[[int], Awaitable[str]],    count: int,) -&gt; None:    tasks = [        asyncio.create_task(operation(i))        for i in range(count)    ]    # Give every task enough time to acquire its semaphore slot.    await asyncio.sleep(0.05)    for task in tasks:        task.cancel()    results = await asyncio.gather(        *tasks,        return_exceptions=True,    )    cancelled = sum(        isinstance(result, asyncio.CancelledError)        for result in results    )    print(f\"Cancelled tasks: {cancelled}\")async def probe(    operation: Callable[[int], Awaitable[str]],    label: str,) -&gt; None:    try:        result = await asyncio.wait_for(            operation(999),            timeout=0.5,        )        print(            f\"{label}: probe completed with {result}\"        )    except asyncio.TimeoutError:        print(            f\"{label}: probe timed out waiting for a semaphore slot\"        )async def demonstrate_bug() -&gt; None:    print(\"=== Buggy implementation ===\")    service = ExternalService(concurrency=3)    # All three tasks acquire one permit and are then cancelled.    # Because release() lives after the await, all permits leak.    await cancel_after_acquire(        service.call_buggy,        count=3,    )    # Nothing is performing network I\/O now, but the new request    # cannot acquire a permit.    await probe(        service.call_buggy,        \"buggy\",    )async def demonstrate_fix() -&gt; None:    print()    print(\"=== Fixed implementation ===\")    service = ExternalService(concurrency=3)    await cancel_after_acquire(        service.call_fixed,        count=3,    )    # Cancellation exits the async-with block and returns permits.    await probe(        service.call_fixed,        \"fixed\",    )async def main() -&gt; None:    await demonstrate_bug()    await demonstrate_fix()if __name__ == \"__main__\":    asyncio.run(main())<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This one bothered me more than the TCP issue because the original code looked reasonable during review.<\/p>\n<p>Acquire.<\/p>\n<p>Perform operation.<\/p>\n<p>Release.<\/p>\n<p>Anyone reading quickly understands the intention.<\/p>\n<p>The missing piece is that async code has more exits than the happy-path indentation suggests. Cancellation can arrive at an <code>await<\/code>, and cleanup cannot depend on execution continuing normally afterward.<\/p>\n<p>The fix was tiny.<\/p>\n<p>The debugging session was not.<\/p>\n<p>And that seems to be a recurring pattern in backend work: the size of a fix has almost no relationship with the amount of reasoning required to find it.<\/p>\n<h3>The veteran programmer was not avoiding tools<\/h3>\n<p>Halfway through the week, something clicked.<\/p>\n<p>At first I had interpreted the older programmer&#8217;s workflow as old-school minimalism.<\/p>\n<p>That was wrong too.<\/p>\n<p>He was using plenty of tools.<\/p>\n<p><code>strace<\/code>.<\/p>\n<p><code>gdb<\/code>.<\/p>\n<p><code>grep<\/code>.<\/p>\n<p><code>git diff<\/code>.<\/p>\n<p><code>git log<\/code>.<\/p>\n<p><code>git bisect<\/code>.<\/p>\n<p>Packet captures.<\/p>\n<p>Database clients.<\/p>\n<p>Tiny reproduction programs.<\/p>\n<p>He just used tools that exposed the system rather than tools that immediately proposed an explanation.<\/p>\n<p>That distinction turned out to be important.<\/p>\n<p>For example, when a process appeared frozen, the temptation was to stare at application logs. But if no application log was being produced, that itself told very little.<\/p>\n<p>A quick <code>strace -p PID<\/code> could show that the process was blocked on a socket, waiting on a futex, repeatedly opening a missing file, or doing something completely different from what the application-level mental model suggested.<\/p>\n<p>The same thing applied to <code>\/proc<\/code>.<\/p>\n<p>When memory looked suspicious, instead of searching for generic Python memory leak advice, checking <code>\/proc\/&lt;pid&gt;\/status<\/code>, open file descriptors and process mappings gave a much narrower starting point.<\/p>\n<p>Git became a debugging instrument too.<\/p>\n<p>This sounds obvious, but <code>git bisect<\/code> is ridiculously effective when two facts are known:<\/p>\n<p>the old revision works<br \/>the new revision fails<\/p>\n<p>If there are 512 candidate commits, binary search needs only about nine decisions to reduce that history to one commit.<\/p>\n<p>That is an absurdly good deal.<\/p>\n<p>Yet in everyday work it is surprisingly easy to spend an hour reading code changed during the last month instead.<\/p>\n<p>The week gradually became an exercise in asking one question:<\/p>\n<p>Can the machine give me a fact before somebody else gives me an opinion?<\/p>\n<p>Usually it could.<\/p>\n<h3>Removing search made the first ten minutes worse and the next hour better<\/h3>\n<p>There is a downside, and it would be silly to hide it.<\/p>\n<p>Some problems became slower.<\/p>\n<p>A strange compiler flag, an obscure library behaviour, an undocumented compatibility issue \u2014 search is excellent for these.<\/p>\n<p>There were moments when the rule felt artificial.<\/p>\n<p>One afternoon I spent twenty minutes discovering something that probably had a perfect three-line answer online.<\/p>\n<p>That was not enlightening. It was just inefficient.<\/p>\n<p>But another pattern appeared.<\/p>\n<p>My normal debugging sessions often started very fast and then became messy.<\/p>\n<p>Search error.<\/p>\n<p>Open five tabs.<\/p>\n<p>Read two GitHub issues.<\/p>\n<p>Try one suggestion.<\/p>\n<p>Change an environment variable.<\/p>\n<p>Restart.<\/p>\n<p>Find a similar exception from a different library version.<\/p>\n<p>Try another suggestion.<\/p>\n<p>Twenty minutes later there are six changes in the working tree and no clear idea which assumption is currently being tested.<\/p>\n<p>Without search, the beginning was slower.<\/p>\n<p>The middle was much cleaner.<\/p>\n<p>By day four, the first few minutes of a bug usually looked like this:<\/p>\n<p>Reproduce it.<\/p>\n<p>Reduce it.<\/p>\n<p>Identify the boundary where correct state becomes incorrect.<\/p>\n<p>Write down the assumptions crossing that boundary.<\/p>\n<p>Add one observation point.<\/p>\n<p>Change one variable.<\/p>\n<p>Repeat.<\/p>\n<p>It sounds painfully basic when written down.<\/p>\n<p>Maybe that is why it is so easy to ignore.<\/p>\n<p>Have you ever noticed how quickly a debugging session turns into code editing? Sometimes the first modification happens before the bug has even been reproduced twice.<\/p>\n<p>That happened to me constantly.<\/p>\n<p>During this week, code changes became almost the last step.<\/p>\n<h3>I also started distrusting error messages in a healthier way<\/h3>\n<p>Error messages are useful, but they describe where a system noticed a contradiction.<\/p>\n<p>They do not necessarily describe where the contradiction began.<\/p>\n<p>The TCP bug produced a JSON exception.<\/p>\n<p>The semaphore leak looked like network latency.<\/p>\n<p>A bad configuration once looked like an authentication problem because the first component capable of rejecting the state happened to be the authentication layer.<\/p>\n<p>This sounds obvious after the bug is solved.<\/p>\n<p>Before it is solved, the exception has enormous psychological gravity.<\/p>\n<p>It gives the problem a name.<\/p>\n<p>And once a problem has a name, the brain starts collecting evidence that supports it.<\/p>\n<p>JSON error becomes JSON problem.<\/p>\n<p>Timeout becomes network problem.<\/p>\n<p>Database exception becomes database problem.<\/p>\n<p>The veteran programmer seemed much less impressed by that naming.<\/p>\n<p>He kept asking about the last known good state.<\/p>\n<p>That turned out to be one of the most useful questions of the week.<\/p>\n<p>If the API response is wrong, was the database row correct?<\/p>\n<p>If yes, was the object correct after deserialization?<\/p>\n<p>If yes, was it correct before transformation?<\/p>\n<p>If yes, was it correct before serialization?<\/p>\n<p>Walk backward until the state stops being wrong.<\/p>\n<p>Or start from the input and move forward.<\/p>\n<p>Either direction works.<\/p>\n<p>What matters is turning a giant system into a sequence of boundaries.<\/p>\n<p>Once the faulty boundary is known, searching becomes much more powerful too.<\/p>\n<p>Now the query is not Python JSON randomly broken.<\/p>\n<p>It is closer to partial TCP reads with length-prefixed protocol.<\/p>\n<p>That is a completely different quality of question.<\/p>\n<h3>The most useful tool was a stupid text file<\/h3>\n<p>No special debugging application won the week.<\/p>\n<p>It was a plain text file.<\/p>\n<p>For every non-trivial bug, I started writing something like this:<\/p>\n<pre><code>SYMPTOMWorker stops accepting new jobs after several hours.KNOWN- Process is alive.- Event loop still runs.- CPU is near idle.- Database responds normally.- New tasks are created but do not reach external_call().UNKNOWN- What are they waiting for?- Is the concurrency limiter exhausted?- Can a permit disappear?HYPOTHESISCancelled tasks leak semaphore permits.DISPROVECancel a task after acquire and check whether another taskcan acquire the same permit.RESULTReproduced in 20 lines.NEXTAudit every manual acquire\/release pair.<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>It feels almost embarrassingly primitive.<\/p>\n<p>But it prevented the debugging process from becoming a pile of half-remembered ideas.<\/p>\n<p>More importantly, it separated facts from guesses.<\/p>\n<p>That distinction gets blurry surprisingly fast.<\/p>\n<p>After forty minutes with a bug, a sentence such as Redis is probably fine can quietly turn into Redis is fine without anyone actually testing it.<\/p>\n<p>Writing things down makes that harder.<\/p>\n<p>It also gives debugging some persistence.<\/p>\n<p>When interrupted by a message or meeting, returning to the bug no longer requires rebuilding the entire mental state from scratch.<\/p>\n<p>For a student project this may seem unnecessary.<\/p>\n<p>For a production incident at 2 AM, it suddenly looks much less silly.<\/p>\n<h3>What I kept after turning the internet back on<\/h3>\n<p>At the end of the seventh day, Google came back.<\/p>\n<p>So did Stack Overflow.<\/p>\n<p>So did AI tools.<\/p>\n<p>There was no dramatic moment where modern software development had been exposed as a mistake.<\/p>\n<p>Search is useful.<\/p>\n<p>AI tools are useful.<\/p>\n<p>A good Stack Overflow answer can save hours.<\/p>\n<p>The experiment did not convince me to stop using any of them.<\/p>\n<p>It changed the order.<\/p>\n<p>Before the week, the workflow was often:<\/p>\n<p>See error \u2192 search error \u2192 try plausible solution \u2192 investigate if necessary.<\/p>\n<p>Now it is closer to:<\/p>\n<p>See error \u2192 reproduce \u2192 identify boundary \u2192 write hypotheses \u2192 collect evidence \u2192 search with a precise question.<\/p>\n<p>That small reordering matters.<\/p>\n<p>AI is also much more useful at the end of that chain.<\/p>\n<p>There is a huge difference between asking why is my asyncio service freezing and providing a minimal reproducer where cancelled coroutines acquire a semaphore and never return the permit.<\/p>\n<p>The second question contains actual engineering work.<\/p>\n<p>And sometimes, once the question becomes that precise, the answer is already sitting in front of you.<\/p>\n<p>The biggest lesson from watching an experienced programmer was not that older developers know more obscure commands.<\/p>\n<p>Of course experience matters. After enough years, certain failure patterns become familiar.<\/p>\n<p>But the more interesting difference was behavioural.<\/p>\n<p>He did not seem desperate to make the error disappear.<\/p>\n<p>He wanted the system to explain itself first.<\/p>\n<p>That is a subtle difference.<\/p>\n<p>Fixing an error and understanding an error are often the same task, but not always.<\/p>\n<p>Modern tools are extremely good at helping with the first one.<\/p>\n<p>The second still requires someone to ask the machine the right questions.<\/p>\n<p>So no, I am not planning another internet-free debugging week.<\/p>\n<p>But one rule survived:<\/p>\n<p>Before searching for the answer, spend ten minutes making sure you actually know the question.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1068210\/\">https:\/\/habr.com\/ru\/articles\/1068210\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A few weeks ago I was sitting next to a programmer who has been dealing with production software for longer than I have been using computers.We were looking at an annoying backend problem. Requests occasionally failed after deployment, but only under load. Restarting the process fixed everything for a while.My automatic reaction was predictable.Search the exception. Check Stack Overflow. Ask an AI tool. Search GitHub issues. Maybe paste the suspicious function somewhere and see what comes back.He did none of that.For maybe fifteen minutes he barely touched the code.He checked the logs, wrote three possible causes in a text file, rejected one of them, added a tiny piece of instrumentation, ran the program twice, then opened the implementation of a library function.At one point I asked whether it would be faster just to search for the error.His answer was simple: first we need to know what we are searching for.That sentence annoyed me a little, mostly because it was obviously correct.A day later I decided to try something stupid: spend one working week debugging without Google, Stack Overflow, Reddit, GitHub issue searches or AI coding assistants.Not permanently. I am not moving into a cabin with a ThinkPad and a printed POSIX manual.Just seven days.The only things allowed were local documentation, official documentation I already knew how to reach directly, source code, tests, logs, Git history and tools already installed on the machine.The experiment quickly became less about living without search and more about noticing how often I normally skip the actual debugging part.The first surprise: I was searching before I had a questionThe first morning was uncomfortable.A Python service threw an occasional JSON decoding error while reading messages from another process over a socket. Normally the exception text would have been copied into a search engine within thirty seconds.Instead I opened the code.The relevant part looked completely harmless:header = sock.recv(4)size = struct.unpack(&#171;!I&#187;, header)[0]payload = sock.recv(size)message = json.loads(payload)A four-byte length prefix, followed by a JSON payload. Nothing exotic.The logs pointed at json.loads, so the first hypothesis was bad JSON.I dumped the received bytes.They were indeed incomplete.That seemed to confirm the hypothesis for about thirty seconds, until the next question appeared: why would the sender produce incomplete JSON?The sender used sendall. The serialized object was correct before transmission. Checksums matched before the write.So the JSON parser was probably innocent.This is where the veteran-programmer habit started making sense. Instead of naming technologies, he had been naming assumptions.My assumptions were:The sender produces one complete frame.The receiver reads four bytes of header.The receiver then reads exactly the number of payload bytes stored in that header.The first statement was easy to verify.The second one was not guaranteed at all.TCP gives us a byte stream. It does not preserve the boundaries of our application messages. A call to recv(4) can return four bytes, but it may also return one, two or three. The same problem applies to the payload.On a local machine with tiny messages, the bug had hidden itself surprisingly well.So instead of searching for Python JSON random error, I wrote a small reproducer.Python \u2014 deterministic test for fragmented TCP readsimport jsonimport randomimport socketimport structimport threadingimport timefrom typing import Optionaldef encode_message(data: dict) -&gt; bytes:    payload = json.dumps(        data,        separators=(&#171;,&#187;, &#171;:&#187;),        ensure_ascii=False,    ).encode(&#171;utf-8&#187;)    return struct.pack(&#171;!I&#187;, len(payload)) + payloaddef send_fragmented(    sock: socket.socket,    frame: bytes,    min_chunk: int = 1,    max_chunk: int = 5,) -&gt; None:    &#171;&#187;&#187;    Deliberately split one application frame into many small writes.    TCP does not promise that the receiver will observe the same    boundaries, but this makes partial reads much easier to reproduce.    &#171;&#187;&#187;    offset = 0    while offset &lt; len(frame):        remaining = len(frame) &#8212; offset        chunk_size = random.randint(            min_chunk,            min(max_chunk, remaining),        )        chunk = frame[offset:offset + chunk_size]        sock.sendall(chunk)        offset += chunk_size        # Make fragmentation visible even on a fast local machine.        time.sleep(0.002)    sock.shutdown(socket.SHUT_WR)def read_message_buggy(sock: socket.socket) -&gt; Optional[dict]:    &#171;&#187;&#187;    This version contains the assumption that caused the real bug:    one recv() call is expected to fill the requested buffer.    &#171;&#187;&#187;    header = sock.recv(4)    if not header:        return None    if len(header) != 4:        raise RuntimeError(            f&#187;Partial header: expected 4 bytes, got {len(header)}&#187;        )    payload_size = struct.unpack(&#171;!I&#187;, header)[0]    payload = sock.recv(payload_size)    if len(payload) != payload_size:        raise RuntimeError(            f&#187;Partial payload: expected {payload_size} bytes, &#187;            f&#187;got {len(payload)}&#187;        )    return json.loads(payload)def recv_exactly(sock: socket.socket, size: int) -&gt; bytes:    &#171;&#187;&#187;    Read exactly size bytes unless the peer closes the connection.    &#171;&#187;&#187;    buffer = bytearray()    while len(buffer) &lt; size:        chunk = sock.recv(size &#8212; len(buffer))        if not chunk:            raise EOFError(                f&#187;Connection closed after {len(buffer)} &#187;                f&#187;of {size} bytes&#187;            )        buffer.extend(chunk)    return bytes(buffer)def read_message_fixed(sock: socket.socket) -&gt; Optional[dict]:    first_byte = sock.recv(1)    if not first_byte:        return None    header = first_byte + recv_exactly(sock, 3)    payload_size = struct.unpack(&#171;!I&#187;, header)[0]    if payload_size &gt; 10 * 1024 * 1024:        raise ValueError(            f&#187;Refusing suspicious frame size: {payload_size}&#187;        )    payload = recv_exactly(sock, payload_size)    return json.loads(payload)def run_once(reader) -&gt; None:    sender, receiver = socket.socketpair()    message = {        &#171;type&#187;: &#171;build_finished&#187;,        &#171;project&#187;: &#171;demo-service&#187;,        &#171;duration_ms&#187;: 1847,        &#171;successful&#187;: True,        &#171;files&#187;: [            &#171;api.py&#187;,            &#171;worker.py&#187;,            &#171;storage.py&#187;,        ],    }    frame = encode_message(message)    thread = threading.Thread(        target=send_fragmented,        args=(sender, frame),        daemon=True,    )    thread.start()    try:        decoded = reader(receiver)        print(&#171;Decoded:&#187;, decoded)    finally:        receiver.close()        sender.close()        thread.join(timeout=1)def main() -&gt; None:    print(&#171;Testing buggy reader&#187;)    try:        run_once(read_message_buggy)    except Exception as exc:        print(            f&#187;{type(exc).__name__}: {exc}&#187;        )    print()    print(&#171;Testing fixed reader&#187;)    run_once(read_message_fixed)if __name__ == &#171;__main__&#187;:    main()The important part was not the recv_exactly function. That function is boring.The useful part was finding the false assumption.Before this experiment, I would probably have searched the exception first. Maybe the correct answer would have appeared immediately. Maybe not.But the search phrase itself would already contain my interpretation of the bug.And my interpretation was wrong.The JSON parser was simply the place where corrupted assumptions finally became visible.How often does that happen in normal debugging? The line that throws the exception gets accused simply because it happens to be holding the body when the murder is discovered.Debugging became much easier when I started writing down what must be trueBy the second day, one habit had changed.Before touching code, I started writing a tiny list:Observed factPossible explanationTest that could make the explanation falseNothing fancy.No elaborate debugging template.For example, another service was occasionally taking thirty seconds to return even though its normal response time was below a second.The obvious suspect was the database.So the first note looked like this:Observed: HTTP request remains open for about 30 seconds.Hypothesis: database query blocks.Test: log timestamps immediately before and after the query.The query took 14 milliseconds.Good. One suspect gone.Next hypothesis: connection pool exhaustion.Test: record pool acquisition time separately from query execution.No problem there either.Next: DNS.Then TCP connect time.Then application semaphore.The difference sounds small, but it changed the whole session. Usually debugging feels like walking through a dark room and touching random objects. Now every command was supposed to kill or strengthen one hypothesis.That also changed how I used logs.Previously, adding logs often meant dumping more state.Now the question became more specific: what is the smallest piece of information that separates hypothesis A from hypothesis B?That is much more useful than adding ten INFO lines and hoping the bug feels guilty enough to confess.There was another unexpected effect.Without a search tab waiting nearby, reading source code stopped feeling like the expensive option.A library behaved strangely, so I followed the call.Then another call.Then one more.Three minutes later the weird behaviour made complete sense.This was probably the biggest psychological change during the experiment. Source code had always been available, but search made it feel slower than it actually was.The nastiest bug of the week looked like a network problemThe second interesting failure happened in an asyncio worker.After several hours, some requests simply stopped progressing. CPU usage was low. Memory looked normal. The database was fine.Restarting the worker immediately fixed it.That is exactly the kind of bug where searching feels irresistible.Asyncio random hang after hours would have been a wonderfully terrible query.Instead, the process got the same treatment as the socket&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-490255","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/490255","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=490255"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/490255\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=490255"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=490255"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=490255"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}