A programming and hobby blog.
While playing around with local LLM inference, I started to internalize how
the web is going to look in the future. This isn’t a hard bet, but the
incentive structure is now clear that Ads will no longer be the preferred way
of monetizing the web. Instead, I see the web continuing to exist as
a pay-per-view model, where most servers reject initial requests with a
405 Payment Required error along with a price.
How We Got Here
Up until about 2023, Google, along with other search engines, was the way
people found information online. Google interleaved ads in the results,
which received clicks, and thus revenue from advertisers. Google’s rise
to prominence stemmed from their ability to distinguish fraudulent clicks from
real clicks from real people. Advertisers picked Google’s ad network over
others due to higher conversion rates, and were happy to pay the premium. This
product is called AdWords.
For the non advertising links, users were redirected to the many websites
across the web. These websites, as they became more popular, needed money to
pay for the rising cost of servers, bandwidth, and staff to keep things
running. For the mass majority of sites, the cost (and traffic) was so low
as to be pennies, and it was just a fun way to be part of the
Internet. For sites that were larger, there were two ways to make money:
- Show ads. These started off as text ads, evolved to flashing GIF ads,
and finally as video and picture ads.
- Require payment. If a user pays for access, they can participate in the
site.
Making payments on the Internet has always been a barrier to entry. For any
early stage website, ads were the only feasible way to go. Charging for
access would alienate the original user base for a site, making it impractical
to ever switch from free or ads-based, over to a payment model.
Google also made this model of sustaining possible. By putting their ad
snippet in your HTML, Google could show ads in the header, footer, or any
other high visibility space. They would figure out who your site’s audience
is, what ads would do best in their slot, and keep track of payment. Once
a month, if you had enough clicks, Google would cut you a check for your
share of the ad revenue. This product is called AdSense.

How We Broke Down
With the proliferation of LLMs, the virtuous cycle of making sites, getting
clicks, earning money, and making more sites, broke down. OpenAI, followed by
Anthropic and others, showed the world there is a better user experience. AI
companies boil down the entirety of the Internet, (and books and everything
else) into a machine that generate text (a.k.a. GenAI). There are two phases
to this process:
- Consume all available information. This is the training phase.
- Use the trained model, and occasionally query for outside data, to
generate text. This is the inference phase.
As the world turns, so too do trained models become out of date. They must
constantly be re-trained with new information (such as news) to be able to
answer topical questions. This has resulted in immense load on the websites
of the Internet, with utterly insatiable demand for all the data available.
Website proprietors saw colossal increases in cost across the board, but with
no humans looking or clicking on their ads.
In other words, almost every site out there is paying higher cost, with
no additional ad revenue to cover.
This might be sustainable in the long run, but I’m betting not. Many
websites have tried to exclude bot traffic, i.e. the machines scraping them,
by using technologies such as Cloudflare and reCaptcha (ironic?). However,
while this may help limit the costs, it doesn’t help with the other
virtuous-cycle breakdown: No more ad revenue.
Google, adopting the UX of OpenAI, started putting their own generated text
at the top of the search results. The starting point for users to visit the
rest of the world wide web is being spawn-camped. I’d bet heavily that
Google’s own outbound clicks from the search engine page is crumbling. With
no more [human] traffic being steered into websites, there is no more ad
revenue to sustain the site. With the ever increasing bot traffic to
websites, the cost is going up. This will not last!
(Aside: I think this change in our technology world is not necessarily a bad
thing. The Tech Industry evolves faster and faster, with outdated ideas
falling before the new innovation. As a user of AI, the LLM snippets are
a better experience. I just want to make sure the Golden Goose isn’t killed
in the transition.)
What Can Be Done To Fix This
Until I started playing around with local LLMs, I kinda knew this world we
live in was not going to last. It seems hopeless to solve, with all the
incentives being upended and no clear part which needs to be changed. The
key idea that made me change my thinking was who was making all these
requests to websites?

LLMs have a weakness: They only know approximate things. Like the
compression artifacts of a JPEG, precise information is hard to capture.
For example, what was the daily high temperature in Los Angeles on August
29th, 1997? The machine knows generally, what temperatures there are,
and generally what temperatures there are in August, and generally
what temperatures there are in Los Angeles. But for specifics, you need
to go the the NOAA [FTP] site to answer this.
Running locally, this problem becomes a lot more apparent. The model itself
is only 20-100 GB in size. Think about that. All the worlds knowledge
boiled down into something that can fit on a thumb drive. Something has
to be removed for it to fit. Unlike the production grade models that
Anthropic, OpenAI, and others run, the local models suffer more from
imprecise knowledge.
To fix this problem, harnesses (like pi.dev) know how to
issue queries to websites and other sources to fill in gaps in their memory.
And, crucially, who is making this query? It’s me. Carl. Not some
faceless megacorp making millions of queries per second, but just me.
We have a chance! Looking back at the virtuous cycle above, website owners
now have a way to ensure they can withstand the influx of load. Not in a
“personalized, hyper targeted panopticon” kind of way, but in a “this is a
real request for knowledge” way.
Carl’s Bet for the Future.
With this in mind, here is what I see for the (hopeful) survival of the web:
Payment Required
Most websites will require a small, almost insubstantial payment for their
services. When accessing a site, it will reject it with
405 Payment Required. Just like a challenge for authentication, our agents
will pay a tiny bit and retry the request with a receipt, finally getting the
data. Scraping the web will flip to being encouraged, with website owners
doing everything they can to invite traffic.
Subscriptions
Rather than every website implementing their own payment solution, I expect there
to be a standardized payment mechanism, perhaps built into the HTTP Spec
itself. Rather than go through the challenge each time, a user will buy a pass
to query an unlimited amount, (perhaps tokens?), and have the cost deducted.
Bulk Data Deals
Reddit was probably the first to sell bulk access to their data. If I recall,
Google paid like $60 Million for unabridged access, in order to train their
model. I suspect deals like this will make sense for much larger sites.
Boutique Data Brokers
Not all data is easily fetched, and some data can only be discovered in person.
I can see a world where custom data is collected and organized by small shops,
and resold for usage by AIs. There will be a large open market for niche data
that LLMs don’t know about, but whose users would like to know. For example:
“Is this restaurant open on Labor Day? Someone can go in person to find out
and publish it via an API.
Where We Go From Here
This change in the world didn’t click for me until I tried
using local LLMs myself. When my harness’s
requests were rejected, I thought “I’d be happy to pay for that, just give me
the data”. Problems such as
overscraping evaporate
when crawlers have to pay for their data. So many problems just disappear. I
think we as a tech community need to agree that the web is in a precarious
state, agree on how we can enable it going forward, and build our tools with
empathy in mind. Website owners need an incentive to keep providing us with
valuable services if we want them to stick around.
Local LLMs have been really interesting to me recently, so much so that I
picked up an NVidia DGX Spark. While I still maintain that reading AI slop
is utterly demoralizing, there are some parts of it that I find help me
a lot. Partly as an exercise in self discovery, and partly to find where
AI is overwhelmingly useful, I have explored using it on some personal
projects. I hate the idea of letting a clanker have all the fun of
programming, but not all programming is unique and exciting.
The biggest litmus test for “Should I use AI for this?” is
“Would I do this myself?”. If you, the reader, wouldn’t personally do
something, then don’t have an AI do it for you. Your agent acts as an
extension of you. Having it do something you wouldn’t means it isn’t you,
yet so many people allow it to use their name, their logins, and thus
their personal brand.
Unit Tests
For example, writing unit tests is pretty dull work. The coding standard
for tests is much lower, with increased grace for things like copy-pasting
code, repetitive set up, barely any documentation, excess field and method
visibility, no immutability rules, etc. We don’t hold test code to the same
standard as regular code.
But, we still write tests. We all agree tests are useful to have. If we
did have time, we would write them. Both at work and at home. Thus,
AI is okay. It still needs a lot of
hand-holding, but it has
accelerated my coding
allowed me to alt-tab to other things while it churns in the background.
I get the most satisfaction out of writing the main code itself, and the
(unit) tests are just insurance that the code does what I think it does.
It doesn’t really make me a better programmer, and it takes time. It’s
easy to zone out while writing unit tests: something that is hard to do
while writing real code.
Caveat: Don’t have the AI write both the main code and the tests. It’s
the fox watching the hen house. Do one, or the other, but not both.
Build Files
I have not, and probably will never, learn how Gradle works. Despite having
used it for nearly a decade, and having hand written thousands of lines of it,
I still don’t get it. I’ve tried. Gradle’s documentation is impenetrable.
It seems like it’s written as a reference, rather than for people who just
kinda know what they want. There is some obscenely complex model for
projects, configurations, tasks, plugins, closures, and what not. However,
knowledge of Gradle, in my experience, can never be boiled down to something
that earns its keep in my working memory. Every few weeks, Gradle releases
another minor version that breaks something one of my plugins depends on.
The huge amount of churn in APIs means there’s pretty much no point in
learning it; it will be different next month.
Thus, my next litmus test: Do I want to get better at this skill?
Am I okay with slowly getting worse at it?
For build files (Gradle, Make, Bazel, etc.), the answer is pretty much no. It
is not my life’s purpose to master building projects. LLMs are extremely
good at reading through all the build documentation for these tools. Hunting
down where this documentation even is is something they excel at. Did I look
at the Gradle blog? Did I make sure to check the git commit notes for
the most recent Bazel release? Was there some note posted on a far off,
newly created git repo that boldly clarifies the new best practices of Bazel?
The answer is no, and it’s a waste of time to go on an expedition every time I
encounter some build issue. LLMs have none of these problems. They are
machines and love rote, methodical work. Best of all, they shed some human
biases, such as assuming they have already checked something. Or
misremembering important details.
Fast Reconnaissance
Recently, I debugged an OOM with a machine learning product I built. While
spelunking through process boundaries and unfamiliar libraries is fun, I also
need to be careful about yak shaving. The issue was how Java, XGBoost, and
OpenMP manage threads. Java code, and the JVM, are both pretty familiar to me
and I can quickly zip through where they might have bad behavior. However,
I only have a novice understanding of how XGBoost and OpenMP interact.
To understand how these pieces work, and if the calendar read “2023”, I would
have Google’d my questions. Hopefully some curious person would have asked
the same question, and some grizzled pro would answer. I would find my
answer on a badly HTML-rendered mailing list, or a Stack Overflow page,
closed for having been useful-but-not-objective. It’s in places like these
that small sparks of insight are found. Even if they aren’t precise matches
to my query, they are enough to unwedge my mind.
Therefore, the last litmus test I’ll proffer is
Would I have Google’d this?. Generally the answer is “yes, obviously!”
I do greatly fear losing my skills, such as no long being able to diagnose
and overcome technical problems. But, if I am being honest, using the LLM
seems substantially equivalent to searching online for someone else having
solved it.
I eventually found out that XGBoost uses OpenMP to create teams of threads,
but has no way to globally limit the number of threads. OpenMP’s docs
confirmed this, and XGBoost provides no way to close the thread pool down.
I worked around the issue by managing the top-level Java threads better,
seeing as neither XGboost nor OpenMP provided a way to fix it. Without
being able to ask complex questions and using local copies of their git
repos, it would have taken a very long time to track down the OOM. That
said, I don’t take the LLMs response for granted. It’s just a machine. We
still need to retain a hint of doubt, and verify what it says is true. This
is especially critical when working in an area I am not familiar with, as
I am more susceptible to subtle, but believable, lies.
Respectful AI Usage
Using AI day to day is still immature in the workplace today. I think we need
to establish etiquette around code generation, commit and PR messages,
and Slack thread chatter. I find it rude when someone’s bot spews a
vomitrocious amount of text into the commit message. It’s de-humanizing how
I asked people questions, and they respond with “Claude said…” (great,
why do you think I couldn’t do that myself?)
Using the first litmus test above (Would I do this myself?) I can’t in good
conscience say I would write a novela’s worth of text in my commit message,
or respond to people’s inquiries with mechanically-separated drivel.

Humans come first. AI is so new, it’s like we are deer staring into
headlights. We are amazed that it can do so much, but yet we forget how
using it makes other people feel. The Golden Rule comes in to play
here: how would I feel if someone hurled slop across the Internet into my
face? If I wouldn’t read it myself, why would I write it? If I wouldn’t
trust it myself, why would I expect others to?
Aside: unfortunately, some companies have proclaimed “Use as much AI as
possible, or else…” I think this has pressured otherwise respectful
people to use LLM slop in improper places.
Conclusion
AI and LLMs have been a marvelous addition to humanity, but we should be
prudent about when and where they are appropriate.
Recently, and for the first time in my career, I have the opportunity to implement a large, highly
concurrent application. After seeing a lot of interest in Java’s new Virtual Threads, I decided
to try them out. I’ve done a decent amount of concurrent programming, so I felt like it would be
relatively easy to get the project started and grow it. From what I encountered, Java’s Future
abstraction is not up to the task.
Without too much backstory of what I looked at, I made a bet: Futures and blocking are the right
abstraction. This means:
Avoid any sort of callback hell. Generally, blocking get() calls are the right way. This is
the same bet that Golang makes, with blocking being the norm. Lean into the scheduler to
make the code work.
Skip flow control. Reactive Java, like Mono and Flux are work arounds for the Java problems
of a decade ago. The stack traces are impossible to understand, and the exception handling in
general doesn’t mesh with the rest of the language. It served its purpose, but avoid it.
Avoid API dependence on CompletableFuture. This class is bloated to the max. Every time I
want to call a method I need to look at the docs, then get mad the code has nearly zero
Javadoc, Look up the CompletionStage for the specification, and finally scratch my head as
to how it’s subtly different than nearly identically named methods nearby. Also,
CompletionStage is practically impossible to implement, and is missing all the useful methods
of Future.
Thus, I decided to make Future<T> the standard return type and interface of choice for my code.
Threads and Futures
In Java, Futures were designed around a thread pool. The idea being work would be scheduled onto
an Executor(Service), and return a Future result which would eventually be populated by another
thread.
Unlike Futures or Promises in other languages (notably Javascript), Futures in Java had the concept
of being associated with a thread. To see why this is interesting, take a look at this method on
the interface:
public interface Future<V> {
/**
* Attempts to cancel execution of this task. ...
*/
boolean cancel(boolean mayInterruptIfRunning);
}
Two things are interesting here:
- Futures can be cancelled. Other languages and libraries often do not support cancellation.
This is a special feature.
- Interruption. Java has a special feature of Threads called
interruption, which allows other threads to ask a sleeping
thread to wakeup, and possibly stop waiting for some event.
This is a very useful thing to have, as it means we don’t have to commit to doing the work in the
thread, should things change. As far as I have seen, almost no other programing model has this
as a core part. Additionally, to implement this yourself, it would be challenging. As a quick thought
experiment, I would ask the reader to think how they would implement this? Using something as
basic as pthread_cond_signal and pthread_cond_wait require holding a lock, which Java’s
implementation doesn’t! How did they do it? (See the link above for an explanation of the
magic).
Thus, Futures, at least when introduced originally, strongly implied attachment to a thread.
Threadless Futures
As of Java 25, there are three main implementations of Futures in the JDK:
FutureTask. This is both a Runnable and a Future, and is intended to be extended. It
holds a reference to the “runner” Thread, which is mounted and unmounted when the task runs
and completes, respectively. Cancelling this Future can interrupt the runner Thread.
CompletableFuture. While I think the implementation is way overengineered, it is the more
powerful of the implementations. It is full featured, and has a solid, reliable way to chain
work together.
ForkJoinTask. This happens to be a Future, but I haven’t seen anyone seriously use it as one.
I mention it here for completeness, but it’s more meant for Fork-Join style work, and less for
complex, heterogeneous work items.
CompletableFuture is the main implementation of interest, since it is capable of building a
general DAG of computation.
Consider the above. CompletableFuture, hereafter “CF”, is a general purpose computation
tool. The dependency graph between Future stages is built dynamically, meaning the whole
graph is not known ahead of time. Each CF can be used to notify multiple downstream CFs.
Two CFs can be used to complete a single downstream CF. The key takeaway here is that any
individual CF does not know what other CFs depend on it.
A consequence of this design decision is that cancellation doesn’t have a clear meaning for
CFs. What does it mean for a CF to be cancelled, and the mayInterruptIfRunning bit is set?
The CF may be a combination of many other CFs. There may be no thread at all attempting to
fulfill a particular CF. The linkage between a CF and a Thread is weakened. As a result,
CompletableFuture does not cancel the underlying thread. (because there may not even be
a thread)
Cancellation and Bi-directionality
Is giving up cancellation that big of deal? Well, maybe. In the world that CF was born into,
threads may not have played as big a role. CF is decidedly push based, despite its predecessor
being pull based. As computations complete, they pop their
Treiber Stack of dependent CFs and fulfill them.
Each downstream CF in the DAG is completed, usually on the thread that is completing the current
CF. (As an aside, this is one of the reasons there are a jillion overloads in CF; they needed
a way to schedule the downstream “callback” work potentially on a different thread.) Keeping
track of which thread is doing the async work may not have been that valuable. Since the idea
of a thread working hard to fulfill a future is gone, where’s the need to interrupt the thread?
Enter Virtual Threads. It’s now possible to have as many Goroutines
Green Threads
M:N Threads Virtual
Threads as you want. They can all block without consequence waiting for CPU or IO bound work
to complete as they patiently await to fulfill a Future. The idea and value of cancellation now
seems more tenable.
CompletableFuture and Chaining
Let’s look at how CFs chain together, in a simple, unidirectional chain.

When CF 1 completes, it notifies (and completes) CF2. When CF2 completes, it notifies CF3. The
flow is from left to right. Control flow only goes one direction. Consider the following
snippet of code:
// Build the HTTP Request
CompletableFuture<HttpRequest> requestFuture =
CompletableFuture.completedFuture(request);
// Issue the request
CompletableFuture<byte[]> httpResponse =
requestFuture.thenComposeAsync(
req -> fetchHttp(req), executor);
// Validate and convert the response
CompletableFuture<MyObject> parsedResult =
httpResponse.thenComposeAsync(
rawJson -> validateAndConvert(rawJson), executor);
System.out.println(parsedResult.get());
Each stage depends on the previous one.
Why Cancellation Matters
Using the snippet above, instead of printing the result, suppose the parsedResult CF is
returned to a caller. Also suppose that the caller is an RPC, and the RPC is cancelled
for whatever reason. We want to cancel the work being done to avoid consuming memory and
threads. How well does this work?
Despite CFs being chained together, they are only chained in one direction! Whoever
cancels the CompletableFuture<MyObject> parsedResult object, it won’t stop the HTTP
request. The parsing future, which has yet to be assigned a thread, has no way to
indicate that the upstream result is no longer needed. In a sense, dependency is a
singly-linked list, with no way to get back to the original CF.

You might suggest that this linkage be added, and the CF class could be made to
propagate cancellation of a downstream CF to the upstream. However, this is where the DAG
property bites us. Consider the following, legal, CF chain:

Cancelling one of the downstream CF’s doesn’t mean the otherones should be.
Without properly cancelling futures, it means that there is a risk of consuming limitted
resources. While it may be okay to do a little extra work if the RPC client cancels their
request, it’s not okay to consume all threads and connection pools on responses that will
never be seen. (In my own work, we saw this result in an OOM due to a runaway executor
that kept adding threads.) Cancellation matter for stability.
Bi-directionality
When thinking through a solution to this problem it becomes obvious that it can’t be solved
by just added a cancel listener to each CF. Someone will eventually forget to add it and
drop the link. The real problem is that the implementation of CF, and the general interface
contract of Future, don’t afford it. Futures do one thing well: defer execution. However,
this is not enough. The true problem is that only results and exceptions flow from one future
to another but not the consumer’s interest in the result.
I have to say I unfairly judged Reactive Java here, with their fully featured cancellation and
flow control mechanics. Originally I had written them off because flow control is only a
seldom useful feature, and primarily between systems, rather than inside them. That said,
flow control is another “consumer interest” signal like cancellation. I guess the implementers
saw that cancellation and flow control nicely unified into a “subscription”, and added both.
I still maintain that flow control is overkill with their request(n) call, but I can clearly
see the value of cancellation propagation.
We do need bi-directionality.
Composability
Given the above history and problems, I now bring my full request: Futures should be composable.
CompletableFuture did a decent job of composition for downstream dependence. However, it is not
enough. We need a way to formally describe the cancellation semantics of asynchronous
computation. It is an error-prone pain in the ass to write this every time:
CompletableFuture<HttpRequest> requestFuture =
CompletableFuture.completedFuture(request);
CompletableFuture<byte[]> httpResponse =
requestFuture.thenComposeAsync(
req -> fetchHttp(req), executor);
httpResponse.whenComplete((_, _) -> {
if (httpResponse.isCancelled()) {
requestFuture.cancel(true);
}
})
CompletableFuture<MyObject> parsedResult =
httpResponse.thenComposeAsync(
rawJson -> validateAndConvert(rawJson), executor);
parsedResult.whenComplete((_, _) -> {
if (parsedResult.isCancelled()) {
httpResponse.cancel(true);
}
});
Manually wiring cancellation is not sustainable.
Execution Context
One additional concern is how execution context is propagated along. In my case, we are using
gRPC. By default, gRPC Java propagates RPC cancellation and deadlines through a thread-local
Context object. One idea for propagating cancellation is to just wire through the cancellation
signal to the root of the dependency tree. For example, if the client RPC triggered the code
above, but then went away, maybe only the end of the dependency chain needs to be cancelled.
If the fetchHttp() call just checked the thread local gRPC context, all the chained futures
between it and the final consumer parsedResult, could be ignored. The root would transitively
cancel all the others.
The problem here is in how CF delegates work to the executor. Each dependent execution stage in CF
only triggers on completion of the source CF. This means the original calling context has been lost
by the time work is scheduled on the executor! To be specific, suppose that
CompletableFuture<HttpRequest> requestFuture was not an immediate, but instead had to be
asynchronously loaded. When it finishes and schedules the HTTP call work, it may do so on it’s
thread, or it may do so on the caller thread. We don’t know. The original gRPC context won’t
be propagated to other threads, since we don’t know how that work was scheduled. In other words,
there is no reliable way to make sure that the calling context is propagated to the async work.
This is why we need full composition with Futures. Between cancellation, deadlines, and execution
context, it’s verbose and error-prone to pass these along reliably.
For those of you designing your own languages and libraries, consider these problems carefully!
CompletableFuture can be used, but it can’t be re-used. When you make your implementation,
make it so that the right thing is the default usually, and custom or specialized behavior doesn’t
become onerous.