You know that feeling when you're building something for yourself and you solve a problem you didn't know you had? Daniel was visiting his in-laws in the US — rural house, unreliable internet, a collection of historical books they wanted catalogued. He extended his home inventory system to handle it, which meant photographing each book seven or eight times: spine, copyright page, inset, sample page. And after a couple of entries, he hit the failure pattern. The record creation was fast — lightweight text — but the media uploads were blocking the whole workflow. Minutes of waiting. The rhythm broke.
And this is the thing — it wasn't the app crashing. The app worked fine. What broke was the human's attention.
Right. So Daniel did what any of us would do at this point — he turned to Claude Code and said fix it. What came out the other side was a background thread runner that caches media locally and uploads when bandwidth allows, plus a server-side job queue that batches AI enhancement through Gemini so it doesn't block record creation. And now he's looking at this thing he built and realizing he hasn't actually reviewed the code. He's asking three questions. One: how does the background upload runner likely work, using IndexedDB and whatever browser APIs make this possible? Two: what's the server-side queuing and batching pattern for the Gemini calls? Three: what does backward batching actually mean — his term — and why is it the key insight? And then the meta-question: how do you review code that an AI wrote when you haven't even looked at it yet?
That last one is the one that's going to keep me up.
So today we're going to reverse-engineer what Claude probably built for Daniel, and why the pattern matters even if your internet is fine.
Let's start with the core tension, because it's not obvious until you've lived it. Data entry wants to be instant. You type, it saves. That's the contract. But media uploads and AI processing are inherently slow — and in connectivity-constrained environments, the gap between fast enough to type and fast enough to upload can be minutes or hours. Daniel's insight was that the failure wasn't technical. It was workflow. The UI blocked on uploads, which blocked the next record creation, which broke the rhythm of photographing and cataloguing. You can't get that rhythm back once it's gone.
And this is different from just offline support. Offline-first apps assume you'll sync when you reconnect — total disconnection, then eventual consistency. Daniel's use case is intermittent connectivity. The internet is there, it's just slow and flaky. The app never goes fully offline, it just... degrades. And the background runner smooths out the latency spikes without ever announcing to the user that anything is wrong.
So we've got three layers to walk through. Client-side caching — the background thread runner using IndexedDB. Server-side job management — queuing and batching those Gemini API calls. And then the -layer of understanding what the AI actually built for you. Three layers, and the third one is where the real discomfort lives.
Let's dig into the client side. What does a background thread runner actually look like in a browser?
Think of it as a NoSQL database that lives inside the browser — it can store structured data and, crucially, binary large objects. Blobs. Images. Depending on the browser, you can store up to about two hundred fifty megabytes per entry before things get dicey. For Daniel's use case — seven or eight photos per book, probably a few megabytes each — that's plenty of headroom.
So the flow is: user creates a book record, the lightweight JSON goes straight to the server, and the images go into IndexedDB.
No, wait. Let me rephrase. The images land in IndexedDB, and the record creation returns instantly. The user has already moved on to the next book. Meanwhile, a Web Worker — that's the background thread — picks up items from IndexedDB and starts uploading them. A Web Worker runs separately from the main UI thread, so even if the upload is crawling along at fifty kilobytes per second, the user never feels it. The UI stays responsive.
And the worker needs to know when to try uploading and when to back off.
Two mechanisms. One is navigator.onLine — that's a browser API that tells you whether the device thinks it's connected. It's not perfect — it can report online when the connection is too slow to be useful — but it's a starting gate. The worker checks it before attempting any upload. The second mechanism is what happens when an upload fails. You don't just retry immediately. You implement exponential backoff — wait one second, then two, then four, then eight, doubling each time, up to some maximum like five minutes. This prevents the worker from hammering a connection that's already struggling.
And Daniel mentioned something interesting — he said newer items get uploaded first. That's a priority inversion from what you'd expect.
It's smart. The user is most likely to need the most recent items visible immediately. If you're cataloguing books and you just entered three in the last ten minutes, those are the ones you might want to pull up and check. The ones from yesterday can wait. So the worker sorts the queue by creation timestamp, descending. Newest first.
That's backward batching in miniature, isn't it? We haven't even gotten to the server side and the pattern is already there.
Let's define that term, because Daniel coined something useful. Traditional batching — what I'd call forward batching — works like this: you collect N items, then process them all at once. Think of an ETL pipeline that ingests a thousand rows, transforms them, loads them into a warehouse. The batch moves forward through the system as a unit. Backward batching inverts this. The item is created instantly. The record exists. The slow side-effects — upload, AI enhancement, thumbnail generation — are deferred and batched behind the scenes. The user never waits for the batch. The batch waits for the user to move on.
So the key insight isn't about technology. It's about which direction the dependency flows. In forward batching, the process depends on the batch being full. In backward batching, the batch depends on the process having already moved on.
And that's why it works for human workflows. Humans don't batch. Humans do one thing, then the next thing, then the next. The system should conform to the human, not the other way around.
Concrete example. Daniel creates a book record in two seconds. Eight photos go into IndexedDB. The background runner uploads them over the next three to seven minutes as bandwidth allows. The server receives the images and queues the AI enhancement — which might take another thirty seconds per book. Daniel has already catalogued four more books by the time the first one's images finish uploading. He never saw a spinner.
I want to touch on Service Workers here, because someone's going to ask why Daniel didn't use the Background Sync API. Service Worker background sync is designed for offline-first apps — the browser registers a sync event, and when connectivity returns, the service worker fires and pushes queued data. It's elegant. But it's built for total disconnection, not variable latency. The sync might fire minutes after reconnection, and you have limited control over timing. For Daniel's use case — intermittent connectivity where the connection is present but slow — a Web Worker with its own upload loop gives you much finer control. You can throttle, you can prioritize, you can back off. Service Worker sync is a blunt instrument; Daniel needed a scalpel.
That's a good distinction. Offline-first and intermittent-connectivity are different problems. Offline-first assumes you're disconnected and will sync later. Intermittent-connectivity assumes you're connected but the pipe is unreliable. Different failure modes, different solutions.
So that's the client side — the background runner, the local cache, the backward batching. But what happens when those images actually reach the server?
That's where it gets interesting, because Daniel added a second layer of deferred processing. The images arrive, and instead of immediately calling Gemini to extract the title, ISBN, and description, the server drops them into a queue.
The likely implementation uses something like Bull or BullMQ — these are Node.js job queue libraries backed by Redis. Redis is fast, in-memory, and handles the atomic operations you need for a queue. BullMQ gives you job states: waiting, active, completed, failed. You get retries with exponential backoff built in. You get delayed jobs — say you want to wait thirty seconds before processing to let all the images for a book arrive. You get priority queues — if a user manually requests enhancement, that job jumps to the front.
And the batching strategy for the Gemini calls.
Instead of firing a separate API call for each book's images, the queue processor can batch — collect five or ten books' worth of images, send them in one request or a tight burst of requests. This reduces API overhead. Gemini's rate limits vary by model tier, but if you're on a free or low-tier plan, you might be limited to a handful of requests per minute. Batching lets you stay under that ceiling while still processing everything eventually.
And the enhancement itself — Gemini extracts title, ISBN, description from the images. That's a round-trip that might take ten to thirty seconds depending on model load. If you did that synchronously while the user waited, you'd be adding insult to injury after the slow upload. By queuing it, the record exists immediately with placeholder fields, and the AI-populated data fills in later.
Daniel said this feature now benefits his home system too, even with reliable internet. And that's the thing — connectivity blips happen everywhere. Your home Wi-Fi drops a packet. Your ISP has a hiccup. The pattern of create immediately, enhance later means the UI never blocks, which is just better UX regardless of network quality. It's not a workaround for bad internet. It's a design principle for any app that does slow background processing.
Let's talk about what Daniel called the uncomfortable question. He hasn't reviewed the code. Claude Code generated it, it works, and he doesn't know what architectural decisions are lurking in there.
This is the -problem, and it's only going to get bigger. User reports suggest Claude Code generates working code about eighty percent of the time on the first attempt. That number is impressive and also terrifying, because the architecture decisions may not match your mental model. The risk isn't just bugs. It's that six months from now, you need to change something, and you discover the AI made a choice you wouldn't have made — and you don't understand why, and you can't easily unwind it.
It's like inheriting a codebase from a developer who never wrote documentation and has since left the company. Except the developer was you, plus a model, and neither of you took notes.
Four practical strategies for reviewing AI-generaed code. One: before you accept the output, ask the AI to explain its architecture in natural language. Not the code — the architecture. What lives where, what talks to what, what are the failure pattern. Claude can do this. It wrote the code; it can describe the design. Two: use diff tools to see what changed file-by-file. Don't just trust that the app works — look at what was added. Three: run the app and test the failure pattern specifically. What happens when the IndexeDB fills up? What happens when Gemini is down? What happens when the upload queue has five hundred items in it? The AI probably handled the happy path. The edge cases are where you find out what it didn't think about. Four: treat AI-generaed code like a PR from a junior developer. You wouldn't merge a junior's PR without reviewing it. Same standard applies.
The junior developer analogy is right. Enthusiastic, fast, sometimes brilliant, occasionally makes decisions that are technically correct but architecturally baffling.
And here's the deeper point. Tools like Claude Code are great at implementation but terrible at explaining tradeoffs. Daniel's app probably works. The backward batching pattern is elegant. But did Claude implement proper error handling for failed uploads? Is there a dead-etter queue for failed AI enhancements? What happens to images that can't be processed — do they sit in IndexeDB forever, slowly consuming storage? These are the questions Daniel should be asking, and the AI won't volunteer the answers unless you ask.
The thing that worries me is the gap between it works and I understand it. That gap is where technical debt lives. And with AI coding tools, you can accrue technical debt faster than ever before, because the code appears instantly. There's no typing time to think about what you're doing.
I want to go back to something I said earlier about the queue implementation, because I think I glossed over an important detail. When I said BullMQ with Redis — that's the likely pattern for a Node.js backend, but Daniel might be using something else. The principle is what matters: a message queue with persistent storage, retry logic, and some notion of job state. You can implement this with PostgreSQL's SKIP LOCKED, with RabbitMQ, with AWS SQS. The specific library doesn't matter as much as the pattern: decouple the fast path — record creation — from the slow path — AI enhancement — with a durable queue in the middle.
The queue itself introduces failure pattern. What if Redis goes down? The queue should be able to recover. What if a job fails five times? You need a dead-etter queue — a separate place where failed jobs go for manual inspection, so they don't clog the main queue with retries. These are the things Daniel should look for when he reviews the code.
Let me add one more thing about the Gemini integration specifically. The API can process images for text extraction, but the rate limits vary by model tier. If Daniel's using the free tier, he might be limited to something like ten or fifteen requests per minute. With the batching pattern, the queue processor can respect that limit — it knows the rate, it spaces out the calls. But what happens when the queue backs up? If Daniel catalogs a hundred books in an afternoon, and each one needs thirty seconds of Gemini processing, that's fifty minutes of queue time. The system handles it gracefully — the records exist, the enhancements trickle in — but Daniel should know that's the expected behavior, not a bug.
That's actually a good segue to the scaling question. Daniel's system works for a home library. What about a warehouse with ten thousand items? The backward batching pattern scales — create immediately, process later — but the queue management becomes a system design problem of its own. You need priority queues. You need monitoring. You need to know when the queue depth is growing faster than the processing rate. Those are second-oder problems that Daniel doesn't have yet, but the pattern supports them if he needs them.
We should talk about one more thing on the client side that I didn't mention. Chunked uploads. Daniel's photographing each book seven or eight times — if each image is five megabytes, that's up to forty megabytes per book. On a slow connection, a forty-megabyte upload that fails at ninety percent is maddening. The background runner should split large uploads into chunks — maybe one megabyte each — and reassemble them on the server. If a chunk fails, you retry just that chunk, not the whole file. This is how every major file upload service works, and it's almost certainly what Claude implemented, but Daniel should verify.
Chunked uploads plus exponential backoff plus priority queuing. That's a lot of moving parts for what started as a book catalguing project.
It's a lot of moving parts for what looks, to the user, like a form that saves instantly. That's the art of this kind of engineering. All the complexity is invisible.
Let's talk about what to actually do when you're staring at a codebase Claude generated and you need to understand it. I think the first step is the natural-language architecture summary. Say to Claude: explain to me, in plain English, what you built. What are the components, how do they communicate, what are the failure pattern. Don't show me code. Show me the design.
Then — this is the part people skip — read the summary and ask yourself whether it matches what you thought you asked for. Daniel asked for a background uploader and server-side queuing. If Claude's summary describes something that sounds different — maybe it implemented a sync engine instead of a queue, or it used a different caching strategy — that's your signal to dig deeper.
Step two: diff the changes. Claude Code modifies files. Look at every file it touched. You don't need to read every line, but you need to know what changed and why. If it added a new dependency — a queue library, a new database table — you should know about it.
Step three: break it. Deliberately. Disconnect the internet mid-uplad. Fill up the IndexeDB. Send a malformed image to the Gemini endpoint. The AI probably tested the happy path. You need to test the sad path. That's where you'll find out if there's error handling or if the whole thing just swallows exceptions silently.
Step four, which is the one I think matters most long-term: write down what you learned. Not documentation for the code — documentation for future you. What architectural decisions did the AI make that surprised you? What would you have done differently? Six months from now, when you need to modify this, those notes are gold.
The uncomfortable truth is that most people won't do any of this. The app works. They'll move on. And for a personal project like Daniel's book catalgue, the stakes are low. The worst case is the queue fills up and some enhancements don't process. But the habit matters. If you get comfortable deploying AI-generaed code without review on personal projects, you'll do it on professional projects too. And that's where the stakes get higher.
There's a distinction I want to draw between code review and architecture review. Code review is line-by-line — does this function handle null inputs, is this loop bounded. Architecture review is: what are the components, how do they talk, what fails when something breaks. AI tools are getting good enough at code that the line-by-line review might eventually become unnecessary. But the architecture review — understanding the shape of the system — that's going to become more important, not less. Because the AI will make architectural choices you wouldn't have made, and you won't notice until you try to change something.
The AI can't tell you why it made those choices. It can describe what it did, but it can't reconstruct the reasoning. That's not how these models work. So you're left with a system that functions correctly and is, in some sense, opaque even to its creator.
I'm not sure about this part, but I think there's something deeper here about the nature of software development changing. For decades, the bottleneck was typing speed — how fast can you translate a design into code. AI removes that bottleneck. The new bottleneck is understanding speed — how fast can you comprehend a system you didn't build. And we don't have good tools for that yet.
The bottleneck moved from production to comprehension. That's a good way to put it.
Daniel's real innovation here wasn't the background uploader or the job queue. Those are standard patterns. It was noticing the failure pattern and inverting the batch. He saw that the system was blocking on the wrong thing, and he restructured the dependency so the human never waits for the machine. That's the kind of insight AI tools can't have, because they weren't there, frustrated, waiting for photos to upload.
The AI can implement backward batching. It can't feel why backward batching matters.
Hilbert: Nineteen ninety-seven. I was digitizing an estate library in Portand — three thousand books, a DSLR, and a hotel Wi-Fi that topped out at one point five megabits on a good day. Uploaded photos one at a time. Took six months.
Hilbert: Daniel's system would have saved me two months. But here's the thing. I actually preferred my terrible system, because I had to look at every single book. Handle it. Photograph it. Type the details. I knew that collection by the end. Knew which books had water damage on page forty-something. Knew which ones had inscriptions. Daniel's system lets you never really look at the books. The AI reads them for you. You're optimizing for speed, but you're also optimizing for not paying attention.
You're romanicizing the grind a little. Daniel photographed each book seven or eight times. Spine, copyright page, inset, sample page. He looked at the books. He just didn't wait for them to upload.
Hilbert: Fair. But the AI enhancement part — the title extraction, the ISBN lookup — that's where the looking stops. He photographed the copyright page, sure. But then Gemini read it. He didn't have to squint at the fine print, figure out whether that's an eight or a three in the ISBN. That squinting is where the knowledge lives.
There's a tension here that I don't think we're going to resolve. Efficiency versus engagement. Daniel's system is objectively better engineering. It handles intermittent connectivity, it never blocks the UI, it scales. But you're not wrong that something is lost when you stop squinting at ISBNs.
Hilbert: I'm not saying it's bad engineering. I'm saying the goal changed without anyone noticing. The goal stopped being cataloguing books and started being finishing the cataloguing. Those are different activities.
Hm.
Hilbert: I still have the spreadsheet from that Portand job. Three thousand rows. Every one typed by hand. I open it sometimes just to look at the titles. Don't open the PDFs of the photos — just the spreadsheet. The spreadsheet is the part I remember making.
The spreadsheet is the artifact of attention.
Hilbert: Something like that.
If you take one thing from this episode, it's that backward batching isn't a technology choice. It's a decision about who waits for whom. The human creates. The system catches up. That inversion — making the machine wait for the human instead of the human waiting for the machine — is the pattern that makes Daniel's app feel fast even when the internet is terrible. And it's a pattern you can apply anywhere slow processes threaten to block fast ones.
The corolary is the review problem. When an AI builds that pattern for you, you inherit not just the code but the architectural decisions embedded in it. The skill of reading those decisions — of understanding what was built and why — is going to be as important as writing code ever was. Maybe more.
Daniel's app works. The question is whether Daniel understands it well enough to change it when the requirements shift. And that question is going to apply to more and more of the software we all depend on.
Thanks to our producer Hilbert Flumingtop. This has been My Weird Prompts. If you want to reach us, email the show at show at myweirdprompts dot com.
We'll be back soon.