There is a good car scene in Green Bay. Not a famous one. But if you know where to look on a Friday night in summer, there are people who have spent more on a turbo than on their first house, and they are all standing around a parking lot arguing about timing.

I drive an Audi Q5 S line, 3.0T, tuned to Stage 2+. Which is a compact way of saying that the car no longer behaves the way the factory shipped it, and that I am responsible for the difference.

That is the thing about a tuned car. Once you have changed how much air and fuel it is working with, the stock dashboard becomes almost useless. It will tell you the coolant is fine and the engine is on. It will not tell you what boost you are actually making, what your intake air temperature climbed to on the third pull, or whether the ECU is quietly pulling timing to protect itself. Those are exactly the numbers that matter once you have modified something, and they are exactly the numbers no factory gauge cluster shows you.

So the moment you want to actually know what your car is doing, you find out that every answer has a price tag attached.

Four apps, four subscriptions, one car

Here is what the landscape looked like when I started paying attention.

If you wanted to know why your check engine light was on, you bought a code reader. BlueDriver runs over a hundred dollars and needs its own proprietary adapter. FIXD puts its best features behind a monthly subscription and requires their sensor. Carly is a yearly subscription, priced per car brand, with the adapter sold separately.

If you wanted to time a 0–60 run properly, you bought a Dragy. That is a GPS box that costs a hundred and fifty to a hundred and eighty dollars and does exactly one thing.

If you wanted a dash cam, that was another two hundred. If you wanted a digital gauge cluster, that was another gadget on the windshield.

So the enthusiast who wants to understand their own car ends up carrying four devices and three subscriptions, each one solving a slice of the same problem, each one talking to the same port under the dashboard.

Every one of those products reads from the same sixteen-pin connector that has been federally mandated on every car sold in America since 1996. They are not competing on access. They are competing on packaging.

That is what bothered me. The data is standardized and free. It has been sitting there for thirty years. What people were paying for was software, and software is the thing I already know how to make.

So the plan was simple to state and difficult to execute: build the Swiss army knife. Codes, live data, drag timing, dash cam, maintenance history, all of it, in one app, on the phone that is already in your pocket. Charge nothing.

The first wall was Bluetooth

I assumed the hard part would be interpreting engine data. It was not. The hard part was talking to the car at all.

Start with the constraint nobody tells you until you have already bought the wrong adapter: on iPhone, your adapter must be Bluetooth Low Energy. Not classic Bluetooth, not WiFi.

The WiFi ones are everywhere and they are cheap and they will not work, because on iOS an app cannot hold a normal WiFi connection to a local device while also expecting the network stack to behave. Classic Bluetooth is worse — the serial profile those adapters use requires Apple’s MFi licensing program, which a hobbyist adapter manufacturer is not part of.

That leaves BLE, which was designed for heart rate monitors and door sensors. Small, infrequent packets from low-power devices. It was emphatically not designed to stream engine telemetry at speed, and you feel that in every design decision downstream.

The second thing nobody tells you is what these adapters actually are. Almost all of them are an ELM327, a chip from the mid-2000s that speaks a text protocol over what is essentially a serial port. You send it a string like 010C, it sends back a string of hex, you parse the hex into an RPM value.

So the real architecture is: a modern phone, speaking a low-energy protocol designed for step counters, to a twenty-year-old serial chip, which translates to a car’s internal bus. Three eras of technology in a chain, and the slowest link sets the pace.

And it is strictly half duplex. One command out, one answer back. You cannot pipeline. You cannot ask two questions at once. Every command in the app goes through a single serialized queue, because the moment two commands overlap on that chip you get garbage that looks almost like valid data — which is worse than an error, because it renders.

The latency problem, stated honestly

Here is the loop you write first, because it is obvious.

You want a live gauge cluster: RPM, speed, throttle, engine load, coolant temperature. Five values. So you ask for five values.

send 010C → wait → parse RPM
send 010D → wait → parse speed
send 0111 → wait → parse throttle
send 0104 → wait → parse load
send 0105 → wait → parse coolant

Each of those is a full round trip: BLE write, chip processes, car’s bus responds, chip formats a text reply, BLE notify comes back. Tens of milliseconds each on a good adapter, considerably worse on a cheap one.

Five round trips per frame. Do the arithmetic and your “live” tachometer updates a few times a second. On a car that revs to 7,000 RPM, a gauge that lags a quarter second is not a gauge. It is a historical record. You blip the throttle and watch the needle respond after you have already lifted.

I want to be clear that this was the moment the project nearly died, because it is not a bug you can fix by writing better code in the normal sense. The round trips are physics. You cannot make BLE faster and you cannot make a 2005 chip think harder.

The only variable you control is how many times you ask.

Batching, and the adapters that lie about it

The OBD2 protocol has a feature that most tutorials never mention: you can request multiple PIDs in a single command.

Instead of five separate conversations, you send one:

010C0D110405

That is RPM, speed, throttle, engine load and coolant temperature, requested together. One round trip instead of five. The response comes back as a single blob that you have to segment yourself, matching each value to the PID that produced it, because the reply does not politely label its parts.

The improvement is not incremental. It is the difference between a gauge that feels connected to the pedal and one that does not.

Then you ship it and discover that adapters lie.

Multi-PID batching is optional. Plenty of cheap ELM327 clones — and a large share of what people actually own are clones — either do not implement it, or claim to and return something malformed. Some cars do not support it on certain protocols. You cannot know in advance. The adapter will not tell you.

So the service has to be empirical about its own capabilities:

  • Try the batch command.
  • If it returns nothing, that might be transient — the bus was busy, the engine was off. Retry next cycle. Do not conclude anything.
  • If it returns data that cannot be segmented, that is a real answer: this adapter does not batch. Mark it permanently and fall back to individual reads for the rest of the session.

That distinction between a transient empty response and a structural incapability took me an embarrassingly long time to get right. Early versions would hit one empty reply while the car was still waking up, permanently decide the adapter could not batch, and then run in slow mode forever — on hardware that supported it perfectly.

The same logic applies per-PID. Not every car exposes every value. An older car may have no boost sensor to read. So each PID gets a failure counter, and after six consecutive no-data responses it is marked unsupported and the app stops asking. That last part matters more than it sounds: every question you ask about a value that does not exist is a round trip stolen from a value that does. The fastest thing a real-time system can do is stop asking pointless questions.

The tuned-car problem, which became a feature

Standard OBD2 gives you a defined set of PIDs that every car must support. It is a useful floor and it is thin.

The things enthusiasts actually care about are frequently not in it. Boost pressure. Intake air temperature. Individual cylinder timing. Transmission temperature. Those live in manufacturer-specific PIDs, which are undocumented, differ by brand, and sometimes differ between model years of the same car.

This is precisely where a tuned car hurts. Stage 2+ means I care about boost and timing more than a stock owner does, and those are the values standard OBD2 is least likely to hand over.

The Q5 is a VAG car — Volkswagen Audi Group — and VAG has its own set. I went looking for boost, and what I found was scattered across forum posts from a decade ago, half of them wrong, most of them untested on my exact engine.

I could have hardcoded the handful I confirmed on my own car and shipped it. Instead, that turned into one of the better pieces of architecture in the app: a PID registry that lives on the server rather than in the binary, plus a discovery service that probes a connected car for which PIDs actually respond with plausible values.

The consequences of that are nice. New PID support does not require an App Store release. A car I have never sat in can contribute what it supports. And the registry gets better as more people use the app, which is the sort of thing that compounds quietly.

It started because I wanted to see boost and timing on my own car. It ended as the mechanism by which the app learns about cars I will never own.

You cannot unit test a car

Most of the app has tests. Command parsing, response segmentation, DTC decoding, reconnect behavior — all of that runs in CI against recorded fixtures, and there is a full mock OBD2 service so the entire app can be developed without a vehicle anywhere nearby.

But a mock will never surprise you, and the actual failures were all surprises.

An adapter that works perfectly in the driveway and drops the connection the moment the alternator comes under load. A car that answers fine with the ignition on but the engine off, then behaves differently once it is running and the bus is busy with real traffic. Values that are correct at idle and implausible under full boost because a parser assumed a byte order that only holds for small numbers.

So a real part of building this was driving. Phone mounted, laptop on the passenger seat, logging raw responses to a file, then pulling over to read them. Blip the throttle, check whether the RPM trace has the shape the pedal had. Watch coolant climb from cold and confirm the curve looks like physics rather than like a parsing error.

The drag timing was worse, because you cannot validate a 0–60 number in a parking lot. That work required actual full-throttle pulls, which requires somewhere legal to do them, which is its own logistics problem in Wisconsin in the months when there is salt on the road.

One decision came directly out of that testing. Most timing apps use GPS, because GPS is easy to get on a phone. But GPS updates once a second, and it lies during hard acceleration because the position fix smooths. AXLY times from the car’s own speed signal over OBD2, which comes from the wheels and updates faster. It costs you a connected adapter and it gets you a number that matches the track.

Then nobody downloaded it

Here is the part that is less fun to write about.

I had an app that did, honestly, more than products people were paying a hundred and fifty dollars for. Codes with plain-English explanations. A full gauge cluster. Drag timing off real vehicle speed. A drive camera that burns telemetry into the video. A garage with service history and spec sheets. No subscription.

And essentially nobody knew it existed.

The App Store is not a marketplace where good products surface. It is a search engine with a very short query and enormous competition for a handful of generic terms. “OBD2 scanner” is contested by companies with advertising budgets. Being free does not help you rank; if anything it signals the opposite of quality to a skeptical browser.

I ran paid social for a while. It brought traffic that did not convert, in geographies I did not want, and the numbers were an education in how much of paid acquisition is buying clicks from people who were never going to install anything.

The thing that actually worked was slower, and it was the same insight as the outreach platform: distribution is a systems problem.

The content engine

People do not search for “OBD2 scanner app” when they have a problem. They search for the problem.

They type P0420. They type why is my check engine light flashing. They type 2016 Golf R oil capacity. Those are enormous, specific, high-intent queries, and the pages that currently answer them are mostly content farms with the same four paragraphs rearranged.

I had something those pages did not: a database of vehicles and codes, and a model that could research a specific question properly.

So I built a content pipeline:

  1. For a given vehicle — year, make, model, submodel — or a given trouble code, run a research pass with Claude using live web search.
  2. Store the result keyed by a slug, in a shared table. Once per slug, not once per user. The second person with a 2016 Golf R is a cache hit, not another model call.
  3. Publish that as a real page on the marketing site: fluid capacities, torque specs, filter part numbers, bulb sizes, maintenance intervals, common problems with mileage.
  4. When new content lands, fire a repository dispatch that rebuilds the static site so the pages are live without anyone touching a deploy.

The site now builds over four hundred pages, and the generation is genuinely automatic — new vehicles and codes become new pages, the site rebuilds on a dispatch or on a daily schedule, and the sitemap updates itself.

Two details made it work rather than making it spam.

The pages are keyed to real data, not to keywords. Each one exists because there is a vehicle or a code behind it, and the content is researched for that specific thing rather than templated around a phrase. A page about P0420 on a Subaru is a different page from P0420 on an Audi, because the answer is different.

The crawlers are explicitly invited. The robots file allows GPTBot, ClaudeBot, PerplexityBot and the rest by name, because a growing share of “what does P0420 mean” never reaches a search results page at all. Somebody asks an assistant, and the assistant cites a source. Being that source is the modern version of ranking first, and you do not get there by blocking the crawlers that would quote you.

That was the inflection point. Not a launch, not a campaign. A slow accumulation of pages that answer the exact question somebody typed at eleven at night with a warning light on the dash, each one ending at an app that will read the code for free.

What I would tell someone starting this

The protocol is the product constraint, not the language or the framework. I spent early weeks on state management and screen architecture when the entire feel of the app was going to be decided by how many round trips a gauge frame costs. Find the physical bottleneck first and design around it.

Distinguish transient failure from permanent incapability. In any system talking to hardware you do not control, conflating those two produces bugs that are nearly impossible to reproduce, because the system’s behavior depends on the order in which it happened to fail.

Stop asking questions you know the answer to. Marking unsupported PIDs and skipping them was a bigger real-world performance win than most of the clever parsing work, and it is the least clever code in the file.

Being free is a pricing decision, not a marketing strategy. It removes an objection. It does not create awareness. I had to build the awareness separately, and that turned out to be a content and infrastructure problem rather than an advertising one.

The car scene part

The original goal was to make some kind of dent in a scene I care about, by taking things that cost money and making them free.

That part worked, in a small way. There are people running drag timing off an app I wrote instead of a box they would have paid for. Somebody cleared a check engine light in a parking lot on a Saturday instead of booking a dealer appointment that starts at a hundred and fifty dollars for the privilege of being told what the code was.

That is a genuinely good outcome, and it is roughly what I hoped for standing in a parking lot in Green Bay wondering why every answer about my own car had a subscription attached.

The unexpected part is how much I learned about distribution from a project that was supposed to be about cars. I set out to write software for enthusiasts and spent a serious share of the effort on how a page gets found, how a crawler reads it, and how an assistant decides what to cite.

Turns out that building the thing is the part I already knew how to do. Everything else was the actual work.


AXLY.pro is free on the App Store and works with any Bluetooth LE OBD2 adapter on any 1996 or newer vehicle. Built in Green Bay, tested mostly in a Stage 2+ Q5.