Pop any UI component into a floating Picture-in-Picture window.

Project: pip-it-up

Why is Picture-in-Picture only for video? Bringing native PiP to React components with pip-it-up

We all love Video Picture-in-Picture. Whether it’s watching a live stream on the side or catching up on a tutorial while you work, floating windows are incredibly useful. But it begs a simple question: why should video have all the fun?

Why can’t we have native PiP for individual UI components?

As a developer who prefers the clean focus of a single-screen setup, I was tired of tab-switching. I wanted a way to tear away just a small, specific part of my web application a markdown editor, a task list, a diagnostic log viewer and stick it always-on-top of my IDE without losing context or breaking my flow.

This is the story of how I built pip-it-up: an open-source React toolkit for the browser’s Document Picture-in-Picture API.

The Document PiP API: more powerful than you think

Most developers are familiar with the standard Video Picture-in-Picture API. It lets you pop a <video> element into a small floating window that stays on top of everything else on your screen. Useful, but limited it only works with video.

The Document Picture-in-Picture API is a different beast entirely. Instead of floating a video element, it opens a fully functional floating browser window that can contain any interactive HTML content. Buttons, inputs, iframes, canvas elements, React components all of it works inside a real, always-on-top floating window.

// The raw browser API
const pipWindow = await window.documentPictureInPicture.requestWindow({
  width: 400,
  height: 300,
});

// Move any DOM node into it
pipWindow.document.body.appendChild(myElement);

On paper this is exactly what we need. In practice, using it directly in a React application is where things get painful.

The three engineering roadblocks

1. CSS stylesheet isolation

When you open a new PiP window, you get a completely blank document. Your Tailwind utility classes? Gone. Your Emotion or Styled Components rules? Gone. Your CSS Modules? Gone.

The PiP window has its own document object with its own <head>. None of the stylesheets from your main application are inherited, which means the moment you move a component into the PiP window, it renders as completely unstyled raw HTML.

This isn’t a minor visual glitch, it completely breaks the component.

2. DOM moving vs. virtual DOM state

Here’s where things get subtle. When you physically move a DOM node from one document into another (which is what the raw API requires), the browser treats it as a brand new element. For vanilla JavaScript this is manageable, but for React it’s a disaster.

React’s reconciler tracks component identity through the virtual DOM tree. When you physically reparent a DOM node into a new document, React loses track of it. The result: the component unmounts and remounts, which wipes all internal state, breaks event listeners, resets scroll positions, resets input cursor positions, and kills any active audio or animation state.

If you had a text input with a cursor halfway through a word, it’s gone. If you had a Monaco editor open with unsaved changes, it’s gone.

3. Browser support

The Document PiP API is currently supported in Chrome 116+ and Edge 116+. Firefox and Safari do not support it at all. Without a fallback strategy, your application simply breaks for a significant chunk of your users.

How pip-it-up solves all three

Real-time style syncing

Instead of hoping styles somehow transfer, pip-it-up uses a MutationObserver to watch your main document’s <head> for any <style> or <link rel="stylesheet"> additions or changes, then dynamically clones and mirrors them into the PiP window’s document in real time.

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.tagName === 'STYLE' || node.tagName === 'LINK') {
        pipWindow.document.head.appendChild(node.cloneNode(true));
      }
    }
  }
});

observer.observe(document.head, { childList: true, subtree: true });

This means Tailwind utilities, CSS-in-JS injected styles, and CSS Modules all work perfectly inside the floating window. The sync is live, if a new stylesheet is injected after the PiP window opens, it gets mirrored automatically.

Virtual tree retention via React Portals

This is the core architectural insight of pip-it-up. Instead of destroying and recreating the React component in the new window, we use a React Portal to physically move the DOM node into the PiP window while keeping the virtual component instance exactly where it was in the tree.

The component never unmounts. React never loses track of it. All context, all state, all event handlers, all cursor positions remain perfectly intact. The component just happens to be rendering its DOM into a different document.

// Simplified version of how Portal Mode works
function PipPortal({ pipWindow, children }) {
  return pipWindow
    ? createPortal(children, pipWindow.document.body)
    : children;
}

From React’s perspective nothing changed. From the user’s perspective, the component is now floating on top of their IDE.

Smart auto-sizing with ResizeObserver

The raw Document PiP API requires you to specify explicit width and height values when opening the window. This means you’d normally need to hardcode dimensions or manually pass them as props — a poor developer experience.

pip-it-up attaches a ResizeObserver to the component being floated and automatically measures its natural dimensions before opening the PiP window, then keeps the window dimensions in sync as the component resizes.

const resizeObserver = new ResizeObserver(([entry]) => {
  const { width, height } = entry.contentRect;
  // Sync PiP window dimensions automatically
  pipWindow.resizeTo(width, height);
});

resizeObserver.observe(componentRef.current);

No width or height props required. It just works.

Graceful fallbacks for Firefox and Safari

When the Document PiP API isn’t available, pip-it-up automatically detects this and falls back to a customizable popup, modal, or inline UI whatever you configure. Your users on Firefox and Safari never see a broken page; they just get a slightly different (but fully functional) experience.

Using it in your project

Installation is straightforward:

npm i @pip-it-up/react

Basic usage with PipWrapper and PipTrigger

import { PipWrapper, PipTrigger } from '@pip-it-up/react';

function App() {
  return (
    <PipWrapper>
      <PipTrigger>
        <button>Float this widget</button>
      </PipTrigger>

      <div className="my-widget">
        <h2>Reference Checklist</h2>
        <ul>
          <li>Review PR comments</li>
          <li>Run test suite</li>
          <li>Update changelog</li>
        </ul>
      </div>
    </PipWrapper>
  );
}

When the user clicks the trigger, the widget tears away into a native always-on-top floating window. Click it again and it snaps back with all its state perfectly preserved.

Floating a Monaco editor with state preservation

import { PipWrapper, PipTrigger } from '@pip-it-up/react';
import Editor from '@monaco-editor/react';

function CodePanel() {
  const [code, setCode] = useState('// Start coding...');

  return (
    <PipWrapper>
      <PipTrigger>
        <button>Pop out editor</button>
      </PipTrigger>

      <Editor
        height="400px"
        defaultLanguage="javascript"
        value={code}
        onChange={setCode}
      />
    </PipWrapper>
  );
}

The editor floats into a native PiP window. Your cursor position, selection state, scroll position, and the code itself all survive the transition perfectly. You can type in the floating editor while your main application is focused in another window.

What you can build with this

A few ideas to get you thinking:

  • Floating reference panels: Keep a keyboard shortcut cheatsheet or API reference visible while you work in your IDE
  • Tear-away chat widgets: Pop a support chat or team messaging widget out of a SaaS tool and keep it on-screen
  • Floating code playgrounds: Let users experiment with code in a persistent floating window
  • Always-on-top dashboards: Pin a metrics panel or log viewer on top of your development environment
  • Persistent note-taking: Float a scratchpad widget that stays accessible regardless of what you’re focused on

Try it

The live playground has interactive demos including a floating Monaco editor, a Tailwind layout widget, and a keyboard shortcut manager all showing real state preservation in action.

If you prefer a focused, single-screen workflow, I hope this helps you stay in the zone. And if you build something interesting with it, I’d genuinely love to hear about it in the comments.

What’s the first component you’d float?

Common Null Safety Mistakes

Dart’s null safety gives you a strong contract, but there are still ways to weaken it in practice.

Flutter’s sound null safety is one of Dart’s most valuable features. When the compiler warns that a value might be null, it is not being overly strict. It is pointing out a real edge case that can surface in production if ignored.

However, null safety does not enforce your business logic. You can satisfy the type system using ?? '', !, or chained ?. operators while still introducing subtle bugs that are often harder to trace than a direct crash like Null check operator used on a null value.

These patterns around nullable fields can quietly build up as technical debt. They may feel convenient or safe at the moment, but they often create hidden problems that show up later in production.

1. Using ?? ” as a silent pass through to repository calls

This is one of the most common patterns and also one of the riskiest.

// ❌ The compiler is satisfied. The API is not.
final result = await profileRepo.updatePhone(phone: user.phone ?? '');

At first glance, this seems safe because the null case is handled. In reality, you have replaced a type safety issue with a semantic one. Most backends do not treat an empty string the same as a missing value.

This can lead to:

  • Overwriting an existing phone number with an empty string
  • Triggering backend validation errors only at runtime
  • Passing validation silently while corrupting stored data

The fallback ?? '' creates a false sense of safety. It tells the compiler everything is fine, but it hides the fact that important information is missing. The null value was meaningful because it indicated the user has not provided a phone number, and that meaning is lost.

A clearer and safer approach is to explicitly handle the null case

// ✅ Explicit handling makes intent clear
final phone = user.phone;

if (phone == null) {
  // Show validation error, log, or stop execution
  return;
}

final result = await profileRepo.updatePhone(phone: phone);

Now Dart promotes phone to a non nullable String, and your intent is explicit. There is no ambiguity about what should happen when the value is missing.

The same issue appears with required IDs

// ❌ Hides missing required data
await orderRepo.fetchDetails(orderId: order.id ?? '');
// ✅ Explicit handling of missing value
final orderId = order.id;

if (orderId == null) {
  // Handle error state appropriately
  return;
}

await orderRepo.fetchDetails(orderId: orderId);

2. Using ! on Text Widget Values

The ! operator is a runtime assertion that says “I am certain this value is not null.” In a Text widget, that certainty is only checked at render time. That can happen after async updates, after a rebuild, after a logout that clears the user model, or while data is still loading.

// ❌ Crashes with "Null check operator used on a null value"
// when the user model is cleared or before profile data loads
Text(viewModel.user!.name!)

When this fails, the error happens inside the build method. Flutter cannot recover cleanly from that, so you get a red screen in development and potentially a broken or blank UI in production.

A safer approach is to provide a meaningful fallback

// ✅ Safe fallback for UI state
Text(viewModel.user?.name ?? 'Guest')

The fallback value like 'Guest' is appropriate here because UI can safely display a default state when data is missing. This is different from backend or repository calls where missing data usually has business meaning.

The key distinction is this: using ?? '' in API calls can silently corrupt data, while using ?? 'Guest' in UI is a deliberate presentation choice.

The same rule applies to widgets that require non-null values

// ❌ Unsafe forced unwrapping
Image.network(product.imageUrl!)
if (product.imageUrl != null) {
  Image.network(product.imageUrl!)
} else {
  const PlaceholderImage()
}

3. Passing null optional fields in query parameters and request payload

When building a Map<String, dynamic> for a GET request with filters or a PATCH request, you need to be intentional about which keys are included. Many REST APIs treat a key with a null value differently from a missing key entirely.

A null value can mean “clear this field” in some backends, or it can trigger validation errors. A missing key usually means “do not change this field.”

❌ Unsafe approach: sending null values

// ❌ Sends {"status": null, "category": null, "page": 1}
// Backend may interpret null as "clear this field"
final params = {
  'status': filter.status,
  'category': filter.category,
  'page': page,
};

The same issue can happen when using `json_serializable` if null inclusion is not configured properly.
// ❌ includeIfNull: true will include null fields in the request body

Safe approach: include only non-null values

// ✅ Only include values that exist
final params = <String, dynamic>{
  'page': page,
};

if (filter.status != null) {
  params['status'] = filter.status;
}

if (filter.category != null) {
  params['category'] = filter.category;
}

Cleaner approach: remove nulls from a map

extension MapNullStrip on Map<String, dynamic> {
  Map<String, dynamic> withoutNulls() =>
      Map.fromEntries(entries.where((e) => e.value != null));
}

final params = {
  'status': filter.status,
  'category': filter.category,
  'page': page,
}.withoutNulls();

Using Dio with query parameters

In Dio, query parameters are passed using queryParameters. The same rule applies: avoid sending null values.

final response = await dio.get(
  '/orders',
  queryParameters: {
    'status': filter.status,
    'category': filter.category,
    'page': page,
  }.withoutNulls(),
);

This ensures only meaningful filters are sent to the backend.

Using Dio with request body (POST or PATCH)

For request payload, the same principle applies.

final response = await dio.patch(
  '/profile',
  data: {
    'phone': phone,
    'bio': bio,
    'avatar': avatarUrl,
  }.withoutNulls(),
);

Using json_serializable for PATCH requests

For PATCH semantics, you usually want only changed fields to be sent. You can enforce this at the model level.

@JsonSerializable(includeIfNull: false)
class UpdateProfileRequest {
  final String? phone;
  final String? bio;

  UpdateProfileRequest({
    this.phone,
    this.bio,
  });
}

This ensures only non-null fields are serialized.

4. Null checking after navigation, not before

In Flutter, navigation passes data through route arguments or constructor parameters. When something is null, the crash often happens in the destination screen, not where the mistake was made. This makes debugging harder because the source of the problem is separated from the failure point.

❌ Unsafe approach: forcing null with !

// ❌ Crashes inside RideDetailsScreen if order.id is null
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => RideDetailsScreen(orderId: order.id!),
  ),
);

This pushes the responsibility of handling null into the widget tree. If it fails, the error shows up far away from where the bug was introduced.

✅ Safer approach: validate before navigation

final orderId = order.id;

if (orderId == null) {
  // Show error, log issue, or stop navigation
  return;
}

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => RideDetailsScreen(orderId: orderId),
  ),
);

This ensures that RideDetailsScreen always receives a valid non nullable value. The screen stays simple and does not need defensive null checks inside its constructor or UI.

The key idea is that the decision about whether navigation is allowed should happen before navigation, not after.

The same pattern with go_router

// ❌ Can lead to invalid or unintended route values
context.push('/order/${order.id}');

✅ Safer approach: validate before navigation

final id = order.id;

if (id == null) return;

context.push('/order/$id');

5. Silent ?? 0 on numeric business fields

Using ?? 0 as a fallback for null numeric values is often misleading in real business logic. While it prevents crashes, it can silently change the meaning of your data.

❌ Unsafe examples: hiding missing data as zero

// ❌ Shows ₦0.00 when price is null, which looks like a free product
Text('₦${product.price ?? 0}')

// ❌ Treats missing values as zero and silently changes the total
final total = cartItems.fold(
  0.0,
  (sum, item) => sum + (item.price ?? 0) * (item.quantity ?? 0),
);

// ❌ Replaces missing ID with a fake value, which can create invalid records
await api.assignRider(userId: rider.id ?? -1);

The core problem is that null is meaningful. It usually represents missing, unknown, or not yet loaded data. Replacing it with zero removes that meaning.

Ask the right question: what does null actually mean here

In most applications, null typically means one of the following:

  • The data is still loading, so the UI should show a loading or placeholder state
  • The field is optional, so absence should be handled explicitly
  • Something went wrong, so an error state should be shown instead of a default value

✅ Safer approach: handle missing values explicitly

// ✅ Clear UI state for missing price
if (product.price == null) {
  const Text('Price unavailable');
} else {
  Text('₦${product.price!.toStringAsFixed(2)}');
}

// ✅ Validate before doing calculations
final price = item.price;
final quantity = item.quantity;

if (price == null || quantity == null) {
  // Skip item, log issue, or handle error state
  return;
}

final lineTotal = price * quantity;

Using ?? 0 is only safe when zero is a truly meaningful default value, such as a display count.

For example:

Text('${product.reviewCount ?? 0} reviews')

In all other cases, especially calculations or API inputs, null should be handled explicitly so that missing data is not silently turned into incorrect business logic.

6. Chaining ?. Without a Final Guard

The safe navigation operator ?. is convenient, but chaining it silently carries null through your code until it hits something that requires a non-null which is often a widget, a map widget, or an API call.

// ❌ lat and lng are double? — passed to a function expecting double
// No crash, but moveCamera silently does nothing or behaves unexpectedly
final lat = order?.rider?.location?.lat;
final lng = order?.rider?.location?.lng;
moveCamera(lat, lng); // What does your map do with null coordinates?

// ❌ Setting a StreamController that might be null — silent no-op
viewModel.order?.id?.let((id) => trackingService.subscribe(id));

The rule: once you’ve chained ?., validate the final result before use.

// ✅ Validate before use
final lat = order?.rider?.location?.lat;
final lng = order?.rider?.location?.lng;

if (lat == null || lng == null) {
  // Show "locating rider..." or return
  return;
}
moveCamera(lat, lng);

For deeply nested optional objects that you access frequently, consider a dedicated getter on the model that handles the chain in one place:

extension OrderLocation on Order? {
  LatLng? get riderLatLng {
    final lat = this?.rider?.location?.lat;
    final lng = this?.rider?.location?.lng;
    if (lat == null || lng == null) return null;
    return LatLng(lat, lng);
  }
}

// Call site is clean and explicit
final position = currentOrder.riderLatLng;
if (position == null) return;
moveCamera(position);

7. Treating nullable booleans as binary values

A bool? is not just true or false. It has three possible states: true, false, and null. The null value usually represents “unknown” or “not yet loaded.” When you use ?? false, you collapse these states into one, and the UI loses important meaning.

❌ Unsafe approach: hiding the unknown state

// ❌ "Not verified yet" and "explicitly unverified" look the same
if (user.isVerified ?? false) {
  showVerifiedBadge();
}

// ❌ A loading or unknown state is treated as false
ElevatedButton(
  onPressed: (viewModel.canProceed ?? false) ? onPressed : null,
)

The issue here is not just correctness, but ambiguity. The UI can no longer distinguish between:

  • The user is not verified
  • The verification status has not been loaded yet

These are different states, but they render the same.

The fix depends on what null actually means

Case 1: null means data is still loading

In this case, you should reflect that explicitly in the UI.

// ✅ Show loading state when value is unknown
switch (user.isVerified) {
  case null:
    return const CircularProgressIndicator();
  case true:
    return const VerifiedBadge();
  case false:
    return const UnverifiedBadge();
}

Case 2: null means a distinct business state

If null represents something meaningful like “unknown” or “not applicable,” then it should be modeled explicitly instead of relying on nullable booleans.

// ✅ Explicit model removes ambiguity
enum VerificationStatus { verified, unverified, unknown }

class User {
  final VerificationStatus verificationStatus;

  User({this.verificationStatus = VerificationStatus.unknown});
}

Now the UI becomes self explanatory and type safe.

When is ?? false acceptable

Using ?? false is only safe when null and false are truly equivalent in meaning. For example, feature flags where absence means disabled.

Even then, that decision should be intentional, not automatic.

Nullable booleans are not just a convenience type. They represent a third state that often carries important meaning. Flattening them into true or false too early can hide real application states and lead to incorrect UI behavior.

Null safety ensures the compiler flags potential null issues, but it is still your responsibility to define what null means in the context of your application. Good null handling shifts that responsibility from “making the code compile” to “making the intent clear.” Your code should communicate to the next developer and your future self what the absence of a value represents and how it should be handled.

When you reach for ?? ”, !, or long chains of ?., it is worth stopping to ask a simple question: what does null represent here, and is this fallback preserving or destroying that meaning?

That moment of clarity is where most null related technical debt is either introduced or avoided.

Should You Use RAG or Fine-Tune Your LLM?

Figure 1: Pinecone pricing for 50M vectors

The debate over retrieval augmented generation (RAG) vs. fine-tuning appears simple at first glance. RAG pulls in external data at inference time. Fine-tuning modifies model weights during training. In production systems, that distinction is insufficient.

According to the Menlo Ventures 2024 State of Generative AI in the Enterprise report, 51 percent of enterprise AI deployments use RAG in production. Only nine percent rely primarily on fine-tuning. Yet research such as the RAFT study from UC Berkeley shows that hybrid systems combining retrieval and fine-tuning outperform either approach alone across benchmarks.

If hybrid systems can produce better results, why does industry adoption favor only RAG? In this article, we’ll compare RAG, fine-tuning, and a hybrid architecture to understand the trade-offs and where each approach excels.

TL;DR

  • RAG: Best for frequently changing knowledge and moderate traffic; easy to update without retraining.
  • Fine-tuning: Best for stable domains and high-volume or low-latency tasks; improves task-specific accuracy and formatting.
  • Hybrid/RAFT: Combines up-to-date retrieval with optimized model behavior for the highest accuracy.
  • Key trade-off: Choice depends on query volume, how often knowledge changes, and team expertise.

Why the Standard RAG vs. Fine-Tuning Comparison Fails

RAG is a method where the model dynamically pulls in external data at inference time. Each query retrieves relevant documents or knowledge chunks, which the system appends to the prompt, allowing the model to produce answers grounded in current information.

Fine-tuning is the process of modifying a model’s weights during training using labeled data. Instead of relying on external retrieval, the model internalizes patterns directly, producing consistent outputs without querying external sources.

While these definitions are technically correct, most standard comparisons miss the factors that actually drive decisions in production. In real-world systems, the choice between RAG and fine-tuning depends on variables like scale, query volume, and how often your data changes.

Missing variable 1: Context expansion at scale

In many production RAG systems, every request appends hundreds of tokens. That added context changes how the model allocates attention and prioritizes weights.

Large retrieved contexts compete for attention with the prompt and instructions, which can dilute signal quality. Small retrieval errors or loosely relevant chunks can introduce formatting drift, or shift reasoning in subtle ways. The system’s output becomes tightly coupled to retrieval quality.

Fine-tuning works differently. Instead of injecting large volumes of text at inference time, it embeds patterns and constraints directly into the model during training. The distinction affects how the system behaves under real workloads.

Missing variable 2: Retraining frequency

The common advice says “use RAG if knowledge changes frequently” and “use fine-tuning if behavior is stable.” But how frequently is “frequently”?

If your knowledge base changes daily, retraining pipelines may introduce operational friction. Evaluation cycles, dataset versioning, and deployment validation all add delay.

Data preparation also matters. If your organization lacks structured, versioned, and clean datasets, the hidden cost of preparing training data can exceed compute costs.

The Cost Math of RAG vs. Fine-Tuning

Surface-level comparisons of RAG and fine-tuning often ignore the cost curves that determine long-term viability. In production systems, financial estimations are crucial in architectural decisions. To evaluate RAG vs. fine-tuning realistically, we need to examine three cost layers:

  1. Token cost and context expansion
  2. Retrieval infrastructure cost
  3. Training infrastructure cost

The cost structure of RAG

RAG systems introduce a recurring operational cost because each query retrieves external information and injects it into the model’s prompt. That additional context is billed on every request.

Context expansion
Production RAG systems append around 500 tokens of retrieved context to each query. The provider bills those tokens on every request.

Using pricing similar to GPT-5.2 at 1.750 dollars per million input tokens, the incremental monthly cost becomes:

Cost per query
500 tokens × $1.75/1,000,000 = $0.000875 per query

At a small scale, this cost appears negligible. However, because it applies to every query, the total overhead grows linearly with traffic.

At different traffic levels:

Monthly queries Context cost
10 million $8,750
50 million $43,750
100 million $87,500

This is context overhead alone. It does not include output tokens or base prompt tokens. At a sustained scale, what appears flexible and inexpensive becomes a significant recurring expense.

Vector database and retrieval cost
Token cost is only one component of RAG costs. RAG also relies on a vector database for semantic search. The system must store, index, and query embeddings efficiently.

Public pricing of Pinecone lists:

  • Storage at approximately 0.33 dollars per gigabyte per month
  • Read units at approximately 16 dollars per million
  • Write units at approximately four dollars per million

For example, consider a system handling 50 million queries per month, where each query performs a single vector search (assuming a 1,024-dimension vector). That would result in 50 million read operations monthly. If the system also writes approximately six million records per month, the combined read and write activity would bring the total estimated monthly cost to around $1,532.

Figure 1: Pinecone pricing for 50M vectors

At 200 million queries per month, the total expenses rises to $9,000 per month.

Two RAG systems serving identical traffic can therefore have materially different cost structures depending on how the vector database is designed and optimized.

Infrastructure cost
RAG systems require storage and compute infrastructure to generate embeddings, store and index vectors, execute retrieval queries, and run inference. Each of these stages consumes compute resources, typically provisioned through cloud servers that must scale with traffic.

For real-time or high-throughput applications, additional capacity is required to maintain low latency and system reliability. Replication, autoscaling, monitoring, and failover mechanisms all add operational complexity. These infrastructure layers are essential for production-grade RAG, but they expand the total cost footprint beyond token usage alone.

The cost structure of fine-tuning

Fine-tuning introduces a different economic model from RAG systems. Instead of paying incremental costs on every request for external context, you invest upfront to modify the model’s internal behavior.

That upfront investment can be broken into four primary cost categories: data, training compute, experimentation, and operational maintenance.

Data preparation costs
High-quality labeled data is the foundation of effective fine-tuning. This includes collecting domain-specific examples, cleaning inconsistencies, formatting inputs and outputs correctly, and validating annotation quality.

In many organizations, data preparation consumes 20 to 40 percent of the total fine-tuning budget. Poorly curated data directly degrades model performance, leading to additional retraining cycles and wasted compute.

Training compute costs
OpenAI lists fine-tuning at roughly $25 per million training tokens for GPT-4.1. A run using 20 million tokens would cost about $500 in direct training fees, with larger datasets or multiple runs increasing this total.

For self-hosted training, costs depend on model size and hardware. High-performance GPUs such as A100 clusters can cost thousands of dollars per training epoch. Because fine-tuning is rarely a single-pass process, multiple epochs, evaluations, and retraining cycles are common, which further increases the overall cost.

Experimentation and validation costs
Fine-tuning is an iterative process that requires experimentation with hyperparameters, evaluation against baseline models, and testing across edge cases. These workflows require engineering time, infrastructure, and structured evaluation frameworks. Unlike prompt engineering, fine-tuning introduces a full ML lifecycle, adding ongoing operational overhead.

This creates a non-linear cost curve. Fine-tuning concentrates cost at the beginning, while marginal cost per request remains relatively stable as traffic grows.

Figure 2: Non linear cost curve

Whether that trade-off is advantageous depends on three variables: query volume, knowledge stability, and retraining frequency. Without modeling those explicitly, cost comparisons between RAG and fine-tuning remain incomplete.

When RAG Wins

Despite its scaling trade-offs, RAG remains the dominant production choice for a reason. In certain operating conditions, it is structurally more flexible, faster to iterate, and operationally safer than fine-tuning. RAG is suitable in the following scenarios:

1. When knowledge changes frequently

If your domain knowledge changes weekly or daily, fine-tuning becomes operationally expensive. Dataset updates, retraining, evaluation, and deployment introduce delays that can stretch from hours to weeks depending on governance requirements.

Teams frequently underestimate the operational overhead of keeping a fine-tuned model synchronized with a rapidly evolving knowledge base. In these environments, RAG shifts the problem from model retraining to data indexing.

2. When you have extensive unstructured data but limited labeled data

Many organizations possess terabytes of internal documents but lack high-quality supervised datasets. Building labeled training corpora requires annotation workflows, domain experts, and quality validation pipelines. In practice, this often becomes the most expensive part of fine-tuning projects.

RAG bypasses this constraint by allowing models to operate directly on existing document corpora without constructing large labeled datasets.

3. When governance and data residency requirements are strict

Once sensitive information is embedded in model weights, deletion and auditing become difficult. Removing a specific record from a fine-tuned model often requires retraining or maintaining complex dataset lineage.

RAG architectures avoid this issue by keeping sensitive information in external storage systems where standard governance controls already exist.

4. When query volume is moderate

As shown in the earlier cost analysis, context expansion overhead grows with query volume, reaching approximately $43,750 per month at 50 million queries. At moderate traffic, RAG’s per-request costs are typically lower than the amortized expenses of fine-tuning, including training and ongoing maintenance. This makes RAG an attractive choice for organizations that want high-quality outputs without front-loading infrastructure and compute investments.

Use cases

Large-scale examples illustrate RAG’s effectiveness at this volume. Notion’s Q&A assistant is effectively a large-scale RAG system over workspace data. The difficult engineering problem was not retrieval itself, but enforcing identity and access controls during retrieval. When a user queries the assistant, the system must ensure the model only retrieves documents that the user is permitted to see.

LinkedIn leveraged RAG and knowledge graphs to preserve the structure of their support cases. This system retrieved relevant subgraphs rather than isolated text chunks, improving retrieval accuracy by 77.6% and reducing median issue resolution time by 28.6%.

For systems at this scale, RAG combines cost efficiency with flexibility, allowing teams to update knowledge sources rapidly without retraining models, while still delivering high-quality results.

When Fine-Tuning Wins

Fine-tuning becomes structurally advantageous under different conditions. These conditions typically involve scale, stability, and behavioral precision.

1. When query volume exceeds 100 million per month

At very high traffic levels (100M+ queries per month), RAG’s per-request context overhead becomes significant. Each query adds hundreds of retrieved tokens that the model processes, causing costs to scale linearly with traffic. Large context windows can also increase latency, reduce throughput, and complicate infrastructure reliability.

If domain knowledge is relatively stable, fine-tuning can become more efficient. By embedding knowledge directly into the model, organizations avoid repeated retrieval and token costs, leading to more predictable per-query expenses, better consistency, and simpler operations at scale.

2. When output structure is critical

Fine-tuned models often excel in tasks that require strict adherence to structure or formal constraints. For example, Cosine, which is an AI software engineering assistant that’s able to autonomously resolve bugs and build features, was able to achieve a SOTA score of 43.8% on the SWE-bench⁠ verified benchmark.

Figure 3: SWE-bench leaderboard

Similarly, Distyl secured the top position on the BIRD-SQL benchmark, widely regarded as the premier evaluation for text-to-SQL performance. Its fine-tuned GPT-4o model reached an execution accuracy of 71.83% on the leaderboard.

Figure 4: Execution accuracy leaderboard

In applications where errors propagate downstream, into financial calculations, automated APIs, or compliance documents, behavioral consistency is mandatory. In these contexts, fine-tuning provides the reliability needed to minimize risk and maintain trust in automated outputs.

3. When latency requirements are strict

RAG adds multiple steps to the inference pipeline that increase response time. Each query must go through embedding generation, vector search, and context injection before reaching the model.

Fine-tuned models skip retrieval entirely. All necessary knowledge and reasoning patterns are internalized, allowing the model to generate outputs immediately. In applications where sub-100ms responses are required, such as live recommendation engines or high-frequency trading systems, removing the retrieval pipeline eliminates a major bottleneck.

4. When deep domain reasoning matters more than freshness

A domain-specific agriculture benchmark study found that fine-tuning improved model accuracy from 75% to 81%, while hybrid systems (fine-tuning + retrieval) reached 86%. Because the dataset focused on specialized agricultural knowledge and reasoning tasks, the improvement primarily reflects stronger domain reasoning, not simply better access to external information.

In domains such as legal analysis or medical decision support, reasoning patterns can be complex. Fine-tuning enables models to internalize domain expertise rather than rely solely on retrieved context.

The Hybrid Approach

While RAG and fine-tuning each have clear advantages, research shows that combining them effectively can produce superior results, but only when done correctly. The RAFT (Retrieval Augmented Fine-Tuning) approach, developed by UC Berkeley, Microsoft, and Meta Research, demonstrates how to do this in practice.

RAFT trains a model to operate in an “open-book” setting. It learns to process retrieved context, identify relevant passages, ignore distractors, and cite evidence accurately. Without this explicit training, simply layering RAG on top of a fine-tuned model often fails. For instance, a model fine-tuned on medical reasoning may retrieve irrelevant journal articles if it hasn’t learned to filter and prioritize context, resulting in hallucinations or incorrect recommendations.

RAFT addresses this with a structured 80/20 training split. 80% of training examples include oracle documents that the model should use, and 20% do not, forcing the model to learn when to trust retrieved data and when to rely on internalized knowledge. This operational detail is crucial for engineers evaluating whether their team can implement a hybrid approach successfully. It is not enough to just combine RAG and fine-tuning. The model must be trained to reason over the retrieved context.

A common and practical pattern is “fine-tune for format, RAG for knowledge.” Fine-tuning shapes the model’s internal behavior, enforcing domain-specific reasoning, output structure, and style. RAG provides dynamic access to external information that changes frequently or is too large to store in the model weights. In healthcare, for example, fine-tuning ensures the model understands medical terminology, follows proper diagnostic reasoning, and formats outputs according to clinical documentation standards. RAG supplements this by retrieving the latest research, newly published treatment guidelines, or patient-specific records, keeping recommendations current without retraining the entire model.

Similarly, Harvey AI fine-tuned on 10 billion case law tokens, but still leverages RAG to handle current cases and updates. This pattern is widely used in other domains too. Legal systems fine-tune for statutory reasoning and citation style, then layer RAG to retrieve the most current case law; finance models fine-tune for portfolio analysis rules, then layer RAG for market updates and regulatory changes. It’s a way to balance the stability of learned behavior with the adaptability of retrieval.

A Quantified Decision Framework for RAG vs. Fine-Tuning

The question is no longer “Which approach is better?” It is “Under what conditions does each approach make economic and operational sense?”

Instead of defaulting to architectural preference, evaluate three measurable variables:

  1. Knowledge change frequency
  2. Monthly query volume
  3. Infrastructure capability and governance constraints

When those variables are quantified, the decision becomes far clearer.

Step 1: Measure knowledge volatility

Knowledge change frequency is often the fastest way to eliminate one option. If your domain knowledge changes weekly or daily, RAG is structurally favored. Updating an index is far simpler than retraining a fine-tuned model. The separation between model weights and external data enables real time data retrieval without redeployment cycles.

If knowledge remains stable for months at a time, fine-tuning becomes economically viable. Retraining frequency drops, and training cost can be amortized over longer intervals. In these environments, embedding domain specific knowledge directly into model parameters may reduce long-term inference overhead.

As a practical threshold:

  • Knowledge changes more than monthly → prioritize RAG
  • Knowledge stable for multiple months → evaluate fine-tuning

Step 2: Calculate context expansion cost**

The next variable is query volume. Large-scale RAG systems append hundreds of tokens to every query, and this context overhead scales linearly with traffic.

Quantitative triggers

Monthly queries Guidance
<10M RAG is cheaper
10–50M Evaluate fine-tuning vs. RAG
50–100M Fine-tuning or hybrid
>100M Fine-tuning or hybrid

Step 3: Assess infrastructure maturity

Even if economics favor one approach, infrastructure capability may dictate feasibility.

RAG requires:

  • Strong data engineering
  • Reliable data pipelines
  • Efficient vector database architecture
  • Observability and monitoring

Fine-tuning requires:

  • High quality labeled data
  • Machine learning expertise
  • Compute resource allocation
  • Evaluation discipline

When teams ignore their actual capabilities, architecture decisions collapse under scale. Many production failures blamed on “model quality” are just traits of immature infrastructure.

Decision matrix

The following matrix translates the analysis into practical guidance.

Scenario Monthly queries Knowledge update frequency Recommendation Rationale
Domain knowledge updates weekly, moderate traffic 10–50M Weekly/Daily RAG Immediate indexing and low recurring cost
High-scale traffic, knowledge stable 50–100M+ <1 update/month Fine-tuning Avoids recurring context injection, reduces latency
Structured output or code generation required Any Any Fine-tuning Embeds domain-specific rules and formatting internally
Specialized reasoning + frequent updates 10–50M Weekly/Daily Hybrid Combines internalized reasoning with dynamic knowledge
Multi-domain systems with diverse knowledge update cycles 10–100M Mixed Hybrid Fine-tuning stabilizes core domains, RAG handles rapidly changing sources

Using this matrix, it becomes easier to make the decision whether to utilize RAG, fine-tune your LLMs, or use the hybrid approach.

Final Thoughts

The debate between RAG and fine-tuning is often framed as a binary choice, but the more useful question is “If hybrid systems demonstrably outperform either approach alone, why does industry adoption still overwhelmingly favor RAG?”

Hybrid requires both ML and data engineering capabilities simultaneously, a combination few organizations have. RAG remains the practical default, offering agility and transparency with less upfront complexity.

The key takeaway is to choose the architecture that matches your knowledge volatility, query scale, and team capability. For teams exploring enterprise-scale retrieval systems, platforms like Actian VectorAI DB provide purpose-built vector database capabilities designed for performance and scalability.

Join the Discord community and learn how Actian fits to your AI strategy.

What 37signals’ Cloud Repatriation Taught Us About AI Infrastructure

Figure 1: Barclay’s CIO survey showing 86% of CIOs planning cloud repatriation

In 2023, 37signals announced that it had completely left the public cloud and followed up by publicly documenting its cloud repatriation process, providing one of the clearest real-world examples of on-premises economics at scale. By reversing its cloud migration and shifting workloads to private cloud infrastructure, the company drastically reduced its annual cloud infrastructure spend by almost $2 million.

The transparency of the numbers made the case compelling. In 2022, 37signals spent $3,201,564 on cloud services, which is about $266,797 per month. These detailed cost breakdowns, along with published hardware investment and payback timelines, provided a rare look into the financial mechanics of large-scale cloud repatriation.

For commodity SaaS workloads, the math was clear. But the same logic raises an important question for the next generation of compute-heavy systems: “Does the economic argument extend to AI infrastructure as well?” In this article, we examine whether the same economic logic holds for AI infrastructure.

TL;DR

  • 37signals spent ~$3.2M/year on AWS in 2022.
  • After repatriating workloads to their own infrastructure, cloud spend dropped to ~$1.3M by 2024.
  • The company invested roughly $700K–$800K in servers and paid them off in under 18 months.
  • The entire infrastructure is still run by the same 10-person team. No additional operational overhead.
  • The key takeaway is that at a sustained scale, owning infrastructure can be dramatically cheaper than renting it.

The 37signals Playbook: What Hanson Actually Documented

In 2022, 37signals spent $3.2 million annually on AWS. After leaving the cloud in 2023, their annual costs had dropped to approximately $1.3 million by 2024, a reduction of almost $2 million per year.

The transition required a hardware investment of roughly $600,000 in Dell servers. The company fully recouped the investment in under 18 months, achieving complete payback in the second half of 2023 as their AWS reserved instance contracts expired. From that point forward, the savings flowed directly to operating margin rather than offsetting capital expense.

37signals projected $1.5 million in hardware costs and roughly $200,000 per year in operating expenses. This shift replaces a recurring $1.3 million annual cloud storage bill with a one-time capital outlay plus a fraction of the ongoing operating cost. Over five years, 37signals revised the total savings projections upward from $7 million to more than $10 million.

37signals cloud exit financials by year

To illustrate the financial impact of 37signals’ cloud exit over time, the table below breaks down annual cloud spending, on-premises hardware investments, and operating costs, highlighting the resulting net savings and key operational notes.

Year Cloud spend Hardware investment Operating costs Notes
2022 Baseline ~$3.2M $0 Included in cloud spend Full cloud dependency
2023 Migration ~$2M ~$700–800K Moderate Hardware fully recouped in under 18 months
2024+ Post-repatriation ~$1.3M ~$1.5M (storage) ~$200K/year ~$1.9M annual savings
2025+ Minimal AWS dependency ~$1.5M (Pure Storage, 18PB) ~$200K/year $10M+ projected 5-year savings

Notably, the migration did not require the team to expand operations. A 10-person infrastructure team handled the entire repatriation without adding new staff. Addressing a common concern about operational overhead, 37signals co-founder David Heinemeier Hansson noted:

“We’ve been out for just over a year now, and the team managing everything is still the same. There were no hidden dragons of additional workload associated with the exit that required us to balloon the team, as some spectators speculated when we announced it. All the answers in our Big Cloud Exit FAQ continue to hold.”

This directly challenges the common assumption that moving away from public cloud environments inevitably requires a significantly larger infrastructure team.

Execution followed a “criticality ladder” strategy where the team migrated lower-risk services first and more critical ones later. The team moved the HEY email system in stages, starting with caching, then database, and finally, job services. To minimize risk, they colocated infrastructure approximately one millisecond from the AWS region to preserve rollback capability during the cloud repatriation process. After stabilizing the system, they replaced managed services with substantial recurring cost, including RDS and managed Elasticsearch which exceeded $500,000 together annually.

What makes 37signals’ case study consequential is the publicly documented cost efficiency. For organizations questioning long-term cloud adoption assumptions particularly with regard to storage costs and managed services, the 37signals documentation provides a rare baseline for comparison.

Why AI Infrastructure Economics Are Even More Extreme

The lessons from 37signals’ cloud repatriation take on a sharper edge when applied to AI infrastructure. Higher GPU costs, predictable inference workloads, massive embedding storage, and stricter data regulations create financial and operational pressures that amplify the advantages of on-premises or hybrid cloud solutions that allow you to move workloads where they make the most sense. Below, we break down the key drivers.

AI infrastructure cost comparison

To evaluate the cost implications of different AI infrastructure approaches, the table below compares upfront setup costs, monthly operating expenses at varying workloads, and expected break-even timelines for cloud, on-premises, and hybrid configurations.

Setup Setup cost Monthly cost Break-even
Cloud GPU rental (AWS/ Azure) $0 $2,900–3,500 (8h/day × $4–8/hour × 15 days) N/A
Cloud inference APIs (Lambda Labs) $0 $1,800–2,500 (8h/day × $3.67/hour × 15 days) N/A
Self-hosted GPU (8×H100 server) $200K–400K $1,500–2,000 (power + maintenance) <12 months
Hybrid (Cloud training + On-Prem) $200K–400K Training only, inference minimal <12 months

Note: For cloud GPU rental, we estimate monthly cost assuming eight hours/day per GPU. The cost scales linearly with utilization; it is not directly per-query.

1. GPU cloud markups are high

AI workloads depend heavily on GPUs, and cloud providers charge far steeper premiums for GPU capacity than for typical CPU compute. On-demand AWS P5 instances with H100 GPUs cost roughly $4–8 per GPU-hour, while comparable Azure H100 instances are about $3.67 per hour. By contrast, spot markets and alternative providers such as Lambda Labs offer similar GPU capacity for $1–2 per hour, or $1.85–2.49 per hour with reserved commitments.

The result is a 4–8× markup for on-demand hyperscaler GPU capacity relative to the spot or specialized GPU cloud market. In other words, the premium cloud providers charge for high-end AI compute is significantly larger than typical CPU cloud markups. For organizations running sustained inference workloads, this pricing gap quickly becomes the dominant cost driver in AI infrastructure.

2. Predictable inference makes GPU ownership economical

High GPU pricing becomes especially significant because AI inference workloads are unusually predictable. Purchasing H100 GPUs outright can be cost-efficient. A single GPU costs roughly $25K–40K, while a complete 8×H100 server ranges from $200K–400K. Lenovo’s analysis shows that six or more hours of sustained daily usage reaches payback against AWS within the first year.

The reason this break-even arrives so quickly is that AI inference workloads are unusually predictable. Unlike SaaS traffic which fluctuates throughout the day, production AI systems such as recommendation engines tend to process steady volumes of requests.

Predictability changes the economics. When infrastructure runs at consistent utilization, owned hardware can be amortized efficiently across the workload. Paying cloud premiums for burst capacity that teams rarely use becomes unnecessary.

For organizations running inference continuously, the hardware investment is often recouped in under 12 months. From that point forward, the savings resemble the same pattern documented by 37signals. Fixed infrastructure replacing an ongoing rental bill.

3. Embedding storage requirements are massive

Even if GPU compute were optimized, AI systems introduce another rapidly growing cost layer: embedding storage. Vector databases store high-dimensional embeddings used for search, retrieval, and recommendation. As datasets scale into millions or billions of records, storage requirements expand quickly.

For instance, 10 million vectors at 1,536 dimensions require at least 58GB of raw storage, often 200–300GB with indexes and metadata. Cloud storage services like Pinecone charge $0.33/GB/month, meaning 500GB could cost $165/month before any queries. Self-hosted solutions like PostgreSQL with pgvector dramatically reduce cloud spending while keeping sensitive data under direct control. Over time, these storage requirements compound infrastructure costs alongside GPU compute, further reinforcing the economic advantages of self-hosted or hybrid architectures.

4. Data sovereignty and compliance favor on-premises deployment

Data residency regulations and general compliance are priorities in the AI space with the industry becoming increasingly regulated. Notably, the EU AI Act introduced strict regulations for AI systems, with prohibitions on certain AI use cases which took effect in February 2025. On-premises deployment simplifies compliance.

For financial organizations navigating complex regulatory environments, solutions like Actian’s Data Intelligence platform helps enforce data governance and streamline compliance workflows.

The Cloud Infrastructure Case Studies 37signals Validated

As much as the financial transparency of 37signals’ cloud exit was radical, their repatriation was not an isolated occurrence. It was part of a growing trend by many organizations trying to regain cost control and optimize their cloud infrastructure. Many high-profile case studies illustrate the scale and economics of moving workloads back from public clouds to owned or hybrid infrastructure.

Dropbox

Dropbox pioneered enterprise cloud repatriation as early as 2015, completing the migration between 2016 and 2018. The company moved roughly 90% of customer data, reportedly over 500 petabytes, off AWS to three owned colocation facilities. The infrastructure investment totaled $53 million, yet Dropbox reported $74.6 million in operational savings over two years per its 2018 S‑1 filing. A small portion of workloads, primarily European customers and specialized services, remain in AWS. Internally, the initiative was known as “Magic Pocket,” and it exemplifies how a well-executed hybrid cloud approach can deliver substantial savings while aligning with long-term business objectives.

Ahrefs

Ahrefs, the SEO tools company, relied on a Singapore colocation setup with 850 servers. Their reported savings from avoiding public cloud were approximately $400 million over 2.5 years. Actual infrastructure cost: $39.5 million for 850 servers (~$1,500/server/month), versus an estimated $447.7 million if hosted entirely on AWS (~$17,557/server/month equivalent). As Ahrefs put it: “We wouldn’t be profitable, or even exist, if our products were 100% on AWS.” While critics argue that Ahrefs inflated AWS estimates, the directional savings were undeniable, illustrating that cloud repatriation challenges can be surmounted at scale with careful planning.

GEICO

GEICO spent a decade migrating to multiple cloud providers only for its costs to climb and exceed projections by 2.5×, reaching $300 million by 2022 across eight providers. In response, GEICO began moving workloads to a private cloud using OpenStack and Kubernetes, targeting over 50% repatriation by 2029. Early results show 50% reductions in compute and 60% reduction per gigabyte of storage costs compared with public cloud services, demonstrating how a hybrid cloud architecture can deliver efficiency, compliance, and alignment with long-term business objectives.

Akamai

Akamai was on the path to spending over $100 million on third party cloud services before migrating compute workloads to its own global edge network of 350,000+ servers. The migration delivered savings of roughly $100 million per year, a testament to the economics of repatriation when existing infrastructure and scale align.

What these cases share is the same economic pattern documented by 37signals. Predictable, high-volume workloads eventually become cheaper to run on owned infrastructure than on hyperscaler clouds.

These examples reflect a broader shift occurring across enterprise infrastructure strategies. Barclays’ Chief Information Officers (CIO) surveys show cloud repatriation trending upward in recent years, with the sentiment peaking in the second half of 2024 with 86% of CIOs planning repatriation.

Figure 1: Barclay’s CIO survey showing 86% of CIOs planning cloud repatriation

However, this statistic does not mean that companies are abandoning public cloud environments completely. According to IDC, only 8–9% of companies favor full repatriation with most preferring a hybrid approach that combines public and private clouds. Hybrid cloud infrastructure allows organizations to optimize workload placement by strategically allocating sensitive data and mission-critical applications on-premises while leveraging public cloud services for less critical workloads. As such, it has become increasingly important for teams exploring similar transitions to understand the nuances of hybrid deployments and their associated risks.

Cloud Repatriation Statistics

Cloud repatriation is accelerating at the same time as public cloud spending keeps climbing. IDC projects global public cloud spend will reach $1.6 trillion in 2028, doubling from their 2024 prediction. Yet as mentioned earlier, 86% of CIOs are planning some form of repatriation according to Barclays. Both trends can be true because this is not a cloud exodus so much as a rebalancing. Enterprises are leaning towards a hybrid cloud model.

AI is likely to accelerate that shift. AI workloads account for less than 10% of total cloud compute today but Gartner projects that this figure will approach 50% by 2029. Hyperscalers are responding with enormous capital investment. There is an estimated $600 billion in infrastructure spend in 2026, roughly three-quarters of it tied to AI. The assumption is clear: Enterprises will rent that GPU capacity. But the 37signals math suggests that once AI workloads move from experimentation to steady production, ownership economics begin to dominate.

Cost pressure is already driving behavior. Flexera reports that 27% of cloud resources are wasted or underutilized, and 21% of workloads have already been repatriated. The primary reason cited is cost exceeding projections, followed by performance concerns. With GPUs, the margin for inefficiency is thinner. There are fewer optimization levers, higher hourly rates, and faster budget burn.

Regulation adds another layer. The EU AI Act, DORA for financial services, China’s PIPL, and India’s DPDP are tightening data governance requirements. Mimecast reports that 87% of organizations now factor data sovereignty into vendor decisions. For AI systems, sovereignty extends beyond data location to model provenance, audit trails, and compliance documentation. On-premises deployment does not eliminate regulatory complexity, but it centralizes control and for many enterprises, that simplicity is becoming strategically attractive.

Figure 2: A bar chart showing why enterprises repatriate

The Counter-Arguments and When Cloud Providers Win

Not all observers agree that cloud repatriation is the best path for every organization. Public cloud environments still deliver value in certain circumstances. But arguments often do not hold strong in the case of AI workloads.

When cloud wins vs. when on-premises wins

Component Cloud advantage On-prem advantage
Workload predictability Handles spiky or unpredictable workloads Predictable workloads cheaper to self-host
Team expertise Requires minimal in-house infrastructure skill Strong IT teams can optimize and reduce vendor reliance
Scale and growth Rapid scaling and global expansion Predictable growth enables cost-efficient hardware
Regulatory requirements Managed compliance, geo-redundancy Direct control simplifies regulatory alignment
Cost and margins Pay-as-you-go reduces upfront spend Long-term savings from owned infrastructure
Service quality Cloud SLAs ensure availability and performance Dedicated resources guarantee predictable uptime

Cloud “wrong usage” argument

Jeremy Daly, a serverless advocate, argues that “37signals was using the cloud wrong.” By treating cloud environments as virtual colocation, running VMs and Kubernetes, they were paying cloud premiums without capturing the value of serverless, managed services, and instant scaling. As Daly notes, “In the cloud, we should be renting services, not servers.”

For SaaS workloads with highly variable or spiky traffic, this argument is compelling. Serverless infrastructure allows organizations to scale instantly and pay only for the compute they actually use.

However, AI inference workloads often behave very differently. Production inference systems, such as recommendation models, copilots, and document processing pipelines, tend to run at steady, sustained utilization rather than unpredictable bursts. In these cases, the economic advantage of elastic cloud scaling diminishes. The premium paid for burst capacity still exists, but the workload itself rarely needs that burst capacity.

Daly’s argument therefore holds for variable SaaS workloads, where elasticity is critical. For sustained AI inference workloads running at high utilization, paying a premium for burst capacity that is rarely used can make dedicated infrastructure or hybrid deployments more cost-efficient.

Full cost critique

Some critics also question the financial assumptions behind 37signals’ approach. They point out that hardware and software normally account for only about 20% of IT costs, with the remainder covering electricity, cooling, physical security, racking, Uninterruptible Power Supply (UPS), and opportunity costs. David Heinemeier Hanson’s analysis did not include all of these overheads because 37signals used colocation facilities rather than fully owned data centers. Even so, considering 37signals’ figures, it is reasonable to conclude that renting colocation space can still be far cheaper than relying on cloud services.

Competence vs. growth framework

Forrest Brazeal’s IT competence versus growth aspirations framework provides additional nuance. He places 37signals in the High Competence/Low Growth quadrant, ideal for self-hosting. “Not every company has the competence (high) or growth aspirations (low) of 37signals,” he observes. Startups with uncertain or spiky workloads benefit from cloud flexibility, but AI companies running production inference at scale often combine high operational competence with steady growth. Such profiles (steady growth & high competence) are well suited to repatriation.

Applying the Playbook to AI Infrastructure

If 37signals provided the economic blueprint, AI infrastructure makes the economics more concrete. The decision is no longer abstract. It becomes a structured assessment grounded in workload behavior, utilization, and regulatory exposure.

A practical four-question framework helps translate the 37signals logic into AI terms:

  1. Is your inference workload predictable and sustained?

    Unlike SaaS traffic spikes, most production AI systems such as recommendation engines, RAG pipelines, or fraud detection models process steady volumes with gradual growth.

  2. Are projected GPU utilization rates above 60–70%?

    At this threshold, owned hardware amortization typically undercuts public cloud GPU pricing within the first year.

  3. Are you processing more than 10–50 million queries per month?

    At this scale, per-token and per-query pricing from cloud APIs compound rapidly.

  4. Do you face data sovereignty or strict compliance requirements?

For financial services, healthcare, or government workloads, regulatory mandates can tilt the decision toward controlled environments.

If the answer is “yes” to three or four of these, the repatriation economics tend to favor on-premises deployment for production inference.

Decision matrix

Workload stage Recommended environment Rationale
Model training Public cloud Compute-intensive; cloud GPUs handle burst workloads cost-effectively
Experimentation and prototyping Public cloud Flexible, fast provisioning for early-stage iteration
Production inference On-premises / Hybrid Steady workloads; owned hardware cheaper at 60–70%+ GPU utilization
Vector storage (embeddings) On-premises Reduces recurring managed-service costs and ensures data control

The hybrid AI pattern

In practice, most AI organizations adopt a hybrid model rather than an all-or-nothing shift. Training remains in the cloud. Inference moves closer to owned infrastructure.

Lenovo documented that training Llama 3.1 at hyperscale (39.3 million GPU hours) in the cloud would exceed $483 million. That type of elastic, short-term scale is exactly where public cloud excels. Inference is different. Once a model is trained, serving it for three to five years becomes steady, predictable work. That is where amortized hardware economics has the upper hand.

This split architecture also simplifies data migration risk. Instead of relocating entire AI pipelines at once, organizations can migrate production inference workloads gradually while leaving experimentation and early-stage training in cloud environments. A controlled, phased migration process reduces operational disruption while ensuring seamless integration between cloud-based training and on-premises serving layers.

Self-hosted inference economics

The economics of self-hosted inference depend heavily on utilization and token volume. According to enterprise deployment benchmarks, a 7B-parameter model running on an H100 GPU at roughly 70% utilization costs about $10,000 per year in spot nodes or hardware amortization. Power costs about $300 annually, bringing the total costs to about $10,300.

Public LLM APIs, by contrast, typically charge per million tokens, with enterprise pricing in 2025 ranging from $0.25–$15 per million input tokens and $1.25–$75 per million output tokens depending on model tier and provider.

At low usage levels, APIs remain the more economical option because infrastructure sits idle. However, the economics change as workloads scale. Industry analyses suggest that self-hosted deployment begins to break even at roughly two million tokens per day, after which the fixed cost of owned infrastructure is amortized across a large inference volume.

At high volumes, self-hosted inference can reduce costs by up to 78%. Artefact’s analysis) found break-even around 8,000 conversations per day. Below that threshold, managed cloud APIs remain more economical. Above it, ownership compounds savings. The pattern mirrors 37signals: predictable workload plus high utilization equals rapid payback.

Vector databases

Instacart documented migrating from Elasticsearch plus FAISS to PostgreSQL with pgvector, achieving 80% cost savings and a 10× reduction in write amplification. Timescale’s pgvectorscale benchmarks show approximately 75% lower costs than managed vector services like Pinecone at comparable performance.

For RAG systems handling millions of queries monthly, self-hosted vector infrastructure produces savings that resemble the 37signals S3 case: large recurring storage bills replaced by amortized hardware and open-source tooling.

Data sovereignty as a structural driver

Grandview research reports that the sovereign cloud market was worth 648.87 billion USD in 2025 and is projected to reach USD 648.87 billion by 2033. Also, according to Gartner, around 60% of financial firms outside the United States are expected to adopt sovereign or on-premises deployments by 2028.

Frameworks such as the EU AI Act, China’s PIPL, and India’s DPDP mandate data localization and traceability. For organizations processing sensitive training datasets or proprietary inference logs, on-premises deployment inherently satisfies residency requirements because data never leaves jurisdictional boundaries.

The Bottom Line

37signals showed that cloud repatriation teams can measure, model, and defend decisions with hard numbers. With AI infrastructure, the economics can be even more pronounced. If cloud repatriation saved roughly $10 million for Basecamp, an equivalent AI company running production inference at comparable scale could save multiples of that amount, given the much higher cost of GPU compute and embedding infrastructure.

For organizations choosing to run AI workloads in controlled environments, platforms like Actian VectorAI DB provide a purpose-built vector database designed for high-volume vector search and AI inference workloads. It can be deployed on-premises or in the cloud, allowing organizations to place vector infrastructure where it best fits their operational and economic requirements.

Join the community and learn more about Actian.

I kept seeing the same OpenClaw mistake: one expensive model for every job

I kept running into the same OpenClaw setup mistake over and over:

people pick one expensive model, wire it in as the default, and then let it handle everything.

Heartbeat checks.
Cron pings.
Inbox triage.
“Nothing changed” loops.
Low-stakes tagging.

That is not a clever agent architecture. That is just an expensive default.

While researching OpenClaw setups, I found a thread on r/openclaw where someone said the quiet part out loud:

“Stop using opus for everything. seriously. i was running it on heartbeat checks and cron pings which is just lighting money on fire. glm-5.1 handles all that stuff fine. i only use sonnet 4.6 now when the task actually needs reasoning and my token costs are like a third of what they were”

That is the right lesson.

Not just for OpenClaw.
For n8n, Make, Zapier, custom Python workers, and basically any agent setup that runs on a schedule.

If you are still using one premium model for every task, you do not have a model strategy. You have a billing strategy you forgot to review.

The actual takeaway: route by task, not by brand loyalty

A lot of developers still treat model selection like a global app setting.

Pick GPT-5.4.
Or Claude Opus 4.6.
Or Gemini 3.5 Flash.
Done.

That works for a demo.
It falls apart in production.

Real agent systems do different kinds of work:

  • cheap classification
  • extraction
  • tagging
  • summarization
  • retries
  • memory maintenance
  • occasional hard reasoning
  • occasional high-risk decisions

Those should not all hit the same model.

The boring jobs should be cheap.
The hard jobs should get the expensive model.
The dangerous jobs should get the model that passes your evals.

That is model routing.
And honestly, it is just basic engineering once your workflows run all day.

OpenClaw already nudges you toward this

One thing I like about OpenClaw is that the config shape already hints at the right mental model.

You can define a primary model and ordered fallbacks.
You can also split out image, PDF, and image generation models.

That is not accidental.
That is the product telling you different tasks deserve different models.

Example:

agents:
  defaults:
    model:
      primary: openai/gpt-5.4-mini
      fallbacks:
        - anthropic/claude-sonnet-4.6
        - google/gemini-3.5-flash
    imageModel: google/gemini-3.5-flash
    pdfModel: openai/gpt-5.4
    imageGenerationModel: openai/gpt-image-1

Once you look at OpenClaw this way, the Reddit advice stops sounding like a hack.
It starts sounding like the intended operating model.

Why people keep wasting frontier models on tiny jobs

Because “agent work” sounds smarter than it usually is.

A heartbeat check feels sophisticated because an agent is doing it.
A cron-triggered inbox review feels important because it uses AI.
But a lot of recurring automation work is just:

  • classify this
  • summarize that
  • compare two notes
  • tag a ticket
  • decide whether anything changed
  • move on

That is exactly where smaller, cheaper models win.

One commenter in the same thread said it perfectly:

“No reason to burn opus tokens on a cron check that runs every 10 minutes.”

Yep.

If a task runs every 10 minutes, you are not choosing a model once.
You are choosing it 144 times per day.
Then you multiply that by every queue, retry loop, mailbox, and background task you forgot was still running.

The pricing spread is big enough that bad defaults compound fast

This is where the mistake stops being theoretical.

Here is the rough shape of the cost difference across common automation-friendly models:

Model What it means for automation work
GPT-5.4 $2.50 input / $15.00 output per 1M tokens; best kept for hard reasoning and high-value steps
GPT-5.4-mini $0.75 input / $4.50 output per 1M tokens; solid default for routine transforms and summaries
GPT-5.4-nano $0.20 input / $1.25 output per 1M tokens; strong candidate for heartbeat checks, classifiers, and cron work
Gemini 3.5 Flash $1.50 input / $9.00 output per 1M tokens; usable for recurring admin tasks and batch workflows

The exact numbers will change over time.
The important part is the spread.

If your default is a frontier model, every low-value task inherits premium pricing.
And retries make it worse.

OpenClaw memory makes this even more obvious

OpenClaw’s memory model is one of the more practical parts of the system.

Instead of pretending memory is some magical hidden state, it writes durable state to files like:

  • MEMORY.md
  • memory/YYYY-MM-DD.md
  • optional DREAMS.md

That means a lot of recurring “agentic” work is really just file maintenance plus lightweight judgment.

Things like:

  • checking whether today’s notes contain anything worth promoting
  • summarizing a session into MEMORY.md
  • tagging daily notes
  • triaging low-priority email
  • deciding whether something needs escalation
  • confirming a scheduled task completed normally

That does not automatically require Claude Opus 4.6 or GPT-5.4.

If the model is reading yesterday’s note, reading today’s note, and deciding whether to append one sentence, you probably want cheap and consistent, not frontier-tier reasoning.

Retries and fallbacks are where expensive defaults get really dumb

OpenClaw supports failover and fallback chains.
That is good.
You want that.

But fallback logic changes the economics.

If your default model is expensive, you do not just overpay once.
You overpay on:

  • the initial call
  • the retry
  • the fallback attempt
  • the loop you forgot to cap

That is why background jobs are dangerous.
They are easy to ignore, and they quietly multiply usage.

A cron task every 30 minutes does not feel expensive.
A cron task every 30 minutes for weeks, with retries, definitely is.

A sane routing policy for OpenClaw

This is the practical version.

Use smaller models for:

  • heartbeat checks
  • cron pings
  • simple classification
  • tagging
  • deduping
  • queue cleanup
  • low-risk summarization

Use mid-tier models for:

  • routine transforms
  • memory promotion drafts
  • support triage with some ambiguity
  • structured extraction with moderate complexity

Use premium models for:

  • hard reasoning
  • ambiguous multi-step tool use
  • sensitive customer-facing responses
  • compliance-sensitive decisions
  • destructive actions

Here is a simple default stack:

agents:
  defaults:
    model:
      primary: openai/gpt-5.4-nano
      fallbacks:
        - openai/gpt-5.4-mini
        - anthropic/claude-sonnet-4.6
        - openai/gpt-5.4

Then override specific tasks that actually need the heavier model.

That setup is less pretty than “we use Claude for everything” or “we standardized on GPT-5.4.”

It is also much more competent.

Don’t route by price alone

Cheap routing can absolutely backfire.

A task can look simple but still be failure-sensitive.

Examples:

  • approving refunds
  • sending customer-facing messages
  • deciding whether to escalate a compliance issue
  • triggering a destructive action in a tool chain

Those should not be assigned by vibes.

Two rules help a lot:

1. Route by consequence, not just complexity

A simple classifier can still be dangerous.
If the output controls money, customer trust, or irreversible actions, treat it as high-risk.

2. Route by evals, not marketing

A cheaper model that fails your real prompts is not cheaper.
It is just a slower way to ship bugs.

If Gemini 3.5 Flash, GLM-5.1, GPT-5.4-nano, or Claude Sonnet 4.6 passes your actual eval set for a task, great.
Use it.
If it fails, move up.

That is routing.
Not ideology.

Quick way to audit your current setup

If you already have OpenClaw running, here is a dead simple audit process.

1. List every recurring task

Make a table for:

  • task name
  • trigger frequency
  • current model
  • failure impact
  • average prompt size
  • average output size

2. Find the obviously overpriced jobs

Look for tasks that are:

  • frequent
  • repetitive
  • low-risk
  • easy to evaluate

Those are your first routing wins.

3. Create a cheap-first policy

Start with a small model for low-risk jobs.
Escalate only if evals say you need to.

4. Cap loops and retries

If a job can retry forever, your pricing model is already broken.

5. Measure before and after

Even a rough comparison is enough:

# pseudo-checklist
# before
# - model: claude-opus-4.6
# - task frequency: every 10 minutes
# - retries: 2
# - monthly usage: painful

# after
# - default: gpt-5.4-nano
# - escalate only on low confidence / failed eval cases
# - monthly usage: much less painful

If you’re building agents at scale, per-token pricing becomes a workflow problem

This is the part people eventually learn the hard way.

Per-token billing is annoying enough in interactive chat apps.
In automations, it is worse.

Because the expensive calls are often not the flashy ones.
They are the boring background ones:

  • scheduled checks
  • agent loops
  • retries
  • nightly summaries
  • queue maintenance
  • memory updates

That is exactly why routing matters.
And it is also why a flat-cost API setup is appealing for teams running lots of recurring agent work.

If your agents are constantly working through n8n, Make, Zapier, OpenClaw, or custom queues, the real pain is not just token price.
It is the constant need to babysit usage.

That is the problem Standard Compute is aimed at.

It gives you an OpenAI-compatible API with flat monthly pricing, so you can keep the routing mindset without getting punished every time your automations actually run.

The useful combo is:

  • route small jobs to cheaper models
  • reserve bigger models for hard steps
  • stop treating every cron task like it deserves frontier pricing
  • stop watching token spend like a stress dashboard

More here if that sounds familiar: https://standardcompute.com

The real tell that someone is new to agents

They brag about which model they use.

People who have actually operated automations for weeks brag about which tasks they stopped wasting expensive models on.

That is why that OpenClaw thread stuck with me.
The useful lesson was not “GLM-5.1 is secretly amazing” or “Claude Sonnet 4.6 is enough.”

It was the shift underneath:

Your agent is a workflow, not a shrine to your favorite model.

Once you see that, model routing stops looking like an optimization trick.
It starts looking like basic competence.

If a heartbeat check is hitting Claude Opus 4.6 every 10 minutes, that is not sophistication.
It is a leak.

And if your setup still uses one expensive model for everything, you probably do not need a better prompt first.

You need a routing policy.

Gemma-San — A Teacher in Every Pocket.

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

Gemma-San is an offline AI tutor for children aged 5–12, built for Nigerian kids first but speaking whatever language the child speaks. It runs Google’s Gemma 4 E2B model fully on-device on a sub-$200 Android phone — no cloud, no data plan after the one-time setup.

A child taps the mic, talks (English, Pidgin, Yoruba, Hausa, Igbo — even Japanese), and Gemma-San responds with voice, illustrations, and patience. It knows when to ask a Socratic question and when to just explain. It remembers the child across sessions. It quizzes them on past lessons. It draws simple SVG diagrams when the topic isn’t already in its illustration library.

Built with Flutter + flutter_gemma 0.15.1 + Whisper.cpp + sqflite. Targets 4–6 GB RAM Android phones like the Tecno Spark 10 and Infinix Hot 30 — the phones African kids actually share with their families.

The problem it solves: millions of kids in low-bandwidth regions have brilliant questions and no one to ask. Cloud tutors fail when the network does. Gemma-San lives inside the phone.

Core features:

  • Voice in (Whisper tiny on-device) → Gemma 4 → voice out (Android TTS)
  • Six native function-calling tools: socratic_teach, direct_teach, encourage, remember, try_drawing, show_illustration
  • 22 hand-built SVG illustrations + an on-the-fly SVG drawing fallback
  • Five-question quizzes and phonics practice with a spaced-repetition scheduler
  • A three-tier memory system (working window + cross-session compaction + long-term facts) so the tutor knows the child
  • Multilingual mirroring — the model replies in whatever language the child used, never imposes one

Demo

Code

Gemma-San

Offline AI tutor for Nigerian children, powered by Gemma 4 on-device.

Hackathon submission — deadline May 18, 2026.

What It Is

Gemma-San is a native Android app that acts as a patient, voice-first tutor for Nigerian children aged 5–12. It runs Google’s Gemma 4 E2B model entirely on-device using flutter_gemma, so it works in classrooms with no internet after the initial model download. The tutor speaks in Nigerian Pidgin or English, uses the phone camera to annotate physical objects, and adapts to each child via a local memory system.

Why

Millions of Nigerian children lack access to quality, personalized tutoring. Gemma-San brings Socratic and direct-teach pedagogy to a $100 Android phone, offline, in the child’s own language.

Architecture

┌─────────────────────────────────────────────┐
│                Flutter UI                   │
│  (Riverpod state, feature-first widgets)    │
└────────────────────┬────────────────────────┘
                     │
        ┌────────────▼────────────┐
        │     Domain / Use Cases  │
        └────────────┬────────────┘
                     │
   ┌─────────────────┼──────────────────┐
   │                 │                  │
┌──▼──┐         ┌────▼────┐       ┌────▼────┐
│sqflite│       │flutter_

…

How I Used Gemma 4

Model: Gemma 4 E2B (litert-community/gemma-4-E2B-it.litertlm).

Why E2B and not E4B or 31B Dense: E4B added roughly 1.5 GB to disk and tripped the OOM killer on my 8 GB target device once Whisper.cpp loaded alongside it. 31B Dense was never an option — these are phones, not workstations. E2B’s 2-billion effective parameters fits in about 2.4 GB on disk, runs at ~25–35 tok/s on the GPU backend via LiteRT-LM, and leaves headroom for STT/TTS/UI. The quality-vs-fit tradeoff was the entire bet.

Native function calling — six tools, no plain text

Every model reply is a function call. There is no free-text path. The system prompt has a Khanmigo-style decision tree the model walks top-down on every turn (visual request → illustration if exact match else SVG → direct fact → Socratic ladder → escalate after 2 IDKs). Side effect: the UI gets a structured TutorResponse with a mode field, so I render a different colored mode pill for each teaching style.

Selective thinking mode (the biggest discovery)

Gemma 4 supports thinking traces via enableThinking: true. I assumed “more thinking = better answers” and switched it on everywhere. That backfired.

Where thinking helps: routing across 6 tools. Without it (and with default topK=1), once Gemma’s first JSON token commits, it can’t recover.

Where thinking hurts: structured-output composition. My lesson-summary path enabled thinking and the model started spending its whole token budget on <|channel>thought|> traces, never reaching the actual lesson_summary function call. Raw thinking JSON leaked into the cached summary and rendered to kids as {"role":"assistant","channels":{"thought":"Thinking"}}.... Beautiful.

I now apply thinking per-path:

Path Thinking Reason
generate() chat ON 6-tool routing benefits from reasoning
generateWithImage() ON Vision + routing
generateLessonSummary() OFF Single tool, pure composition

And I always pass explicit sampling — the defaults are a trap for small models:

final session = await model.createSession(
  tools: tools,
  systemInstruction: sysPrompt,
  enableThinking: true,
  temperature: 0.4, // not 0.8 default — too noisy for tool selection
  topK: 40,         // not 1 default — pure greedy locks in wrong tools
  topP: 0.9,
  randomSeed: 1,
);

Compressing the prompt for a 2B model

My first system prompt was 1,450 tokens — a beautifully written teaching ladder. Gemma E2B’s attention horizon couldn’t hold it together with 6 tool schemas and conversation history. It misrouted, hallucinated tool names, and sometimes emitted plain text.

Rewriting it as a flat ~440-token decision tree with two worked examples (one Socratic, one direct-teach with the child speaking Pidgin and the model mirroring it back) took tool-call accuracy from “frustrating” to “actually usable.”

Multilingual mirroring

Hard rule in the prompt: whatever language the child uses, the model uses back. English in, English out. Pidgin in, Pidgin out. Yoruba, Hausa, Igbo, Japanese — match it. The language_code BCP-47 field on every tool call propagates to Android TTS so the voice matches too (Google TTS preferred when installed; en-NG > en-GB > en-US fallback).

Recovery loop, never an error

When parsing fails (rare, but real on a 2B model), Gemma-San retries once with a nudge appended — "Respond by calling exactly ONE function. No plain text." — and falls back to a graceful direct_teach if the retry also fails. The child never sees an error message.

Lessons Learned

  1. Thinking mode is a knife, not a hammer. ON for routing. OFF for composition. Mixing them in the same session causes JSON leaks and budget exhaustion.

  2. topK=1 + thinking is the worst sampling combo on a small model. Pure greedy decoding can’t recover from a bad first token even when the thinking trace has already considered alternatives. Bump topK to ~40 and temperature to ~0.4 for tool calling.

  3. Compress your prompt brutally. A 2B model with 6 tool schemas + working memory + a 1,400-token system prompt has nothing left for attention. A flat Khanmigo-style decision tree with one or two worked examples beats verbose pedagogy every time.

  4. Validate, don’t reject. My SVG validator auto-fixes the model’s malformed viewBox, injects missing fill attributes, and only rejects what truly can’t render. Treat the model as a fragile partner, not a black box.

  5. Mirror, don’t impose. Telling the model “respond in Pidgin” forced bad output when kids spoke English. Telling it “mirror the child’s language” produced fluent code-switching across five Nigerian languages out of the box.

Built for the Gemma 4 Challenge. Feedback welcome — especially from anyone shipping on-device LLMs for low-resource contexts.

The top 15 developer productivity tools in 2026

Port developer tool with custom pricing for enterprises, homepage screeenshot.

Meta: Discover the 15 best developer productivity tools in 2026. Compare AI coding agents, automation, code review, and engineering intelligence platforms.

The rise of developer productivity tools is closely linked to the widespread adoption of AI in engineering. A 2026 Cortex survey, which gathered data from 50 engineering leaders across North America, Europe, and Asia-Pacific in companies with over 500 employees, found that nearly 90% of them reported their teams are actively using AI, with implementation ranging from individual use to mandatory adoption across all teams.

The market reflects this growth, as projections anticipate the global software development tools market will reach $22.6 billion by 2033.

With hundreds of options available today, the challenge lies not in finding a tool, but in selecting the right one for an individual or a team. To simplify tool selection, we will categorize the options and then highlight the best choice within each category, a method used throughout this article.

Why developer productivity tools matter

Developers would ideally dedicate all their work hours to coding. However, the reality is that their time is split among various essential, non-coding tasks. These often include diagnosing and fixing bugs, conducting code reviews, managing software dependencies, and creating or consulting documentation.

Furthermore, their schedule is also filled with strategy discussions, meetings with non-technical stakeholders, client communications, and administrative duties.

A few quick stats to drive these points home:

Clearly, something is very wrong. When developers spend the vast majority of their time engaging with tasks other than coding, their focus spills, team morale erodes, and productivity suffers across the board.

This shows why productivity tools for developers can make a huge difference to workflows. They automate repetitive tasks, reduce cognitive load, strengthen collaboration, provide insights, and improve the overall developer experience.

The four types of developer productivity tools

We have categorized the developer productivity tools below into four main groups. These categories reflect how the tools assist software development teams in achieving faster, higher-quality code, optimizing their workflows, and ensuring consistent quality standards throughout the entire development lifecycle.

Productivity and engineering intelligence platforms

Productivity and engineering intelligence platforms aggregate data from Git repositories, CI/CD pipelines, and project tracking tools in order to provide insights into a developer team’s use of resources and overall effectiveness.

Port, DX, Jellyfish, Linearb, Harness, Axify, and Atlassian Compass stand out among productivity and engineering intelligence platforms available on the market today. They collect and analyze data, turning raw commits, tickets, and build logs into actionable metrics like, for example, code churn and cycle time. Consequently, they help benchmark a team’s performance and improve workflows.

With dashboards and alerts, these tools for developer productivity help keep projects on track, surfacing issues like stalled pull requests or delayed testing, helping organizations to optimize internal processes better.

AI coding agents

AI coding agents, such as Claude Code, Cursor and Copilot, use large language models (LLMs) and deep learning to write and refactor code. They can work autonomously across projects and understand context spanning multiple files and repositories, generating, testing, reviewing and debugging code.

AI coding agents are used to automate a wide range of repetitive and routine tasks, letting developers to focus on more complex work. For example, a developer might use an AI coding agent to automate code reviews or identify bugs and find ways to fix them.

These AI tools for developer productivity integrate directly into workflows and can even serve as personal coding assistants, engaging in conversation with developers, explaining code to new hires, and guiding them through unfamiliar parts of a project.

Automation tools

Automation tools serve an important purpose in the software development lifecycle, reducing the manual steps a developer team needs to make. This helps developers test, build, and deploy software faster. Tools such as Gradle, Postman, and GitHub Actions, among others, fall into this category.

By eliminating repetitive tasks such as running tests on each code commit or automatically deploying successful builds to staging environments, automation tools reduce human error and improve efficiency.

They can, for example, merge pull requests without human intervention, which helps reduce bottlenecks.

Automation tools deliver real productivity gains by allowing engineers to iterate rapidly and safely, while handling tedious tasks on their own.

Code review and code quality tools

Code review and code quality tools, like CodeRabbit and Crucible, enforce coding standards early in the software development process, catching defects such as bugs, security vulnerabilities, or style violations.

By providing continuous and automated feedback on code health, these productivity tools for developers help teams reduce the risk of failure, blocking releases when issues are detected so that the software is stable and secure.

More often than not, code review and code quality tools come with collaboration features like inline comments and threaded discussions to improve communication and collaboration among developers. They also use AI assistants to identify bugs, suggest improvements, and automate fixes.

The 15 best productivity tools for developers

Now that we’ve covered the ground concerning the importance and different types of productivity tools, here are the 15 best productivity tools for developers in 2026.

1. Port: Unified internal developer portal

Port developer tool with custom pricing for enterprises, homepage screeenshot.

Port provides an internal developer portal that centralizes resources, unifying all tools, services, and documentation your team uses. It offers a single control plane to browse microservices, APIs and infrastructure.

Key features and benefits:

  • Searchable registry of all components

  • One-click actions integrated via CI/CD and Terraform

  • Automated scorecards for security and compliance

  • Connects to hundreds of tools out of the box

2. DX: Data-driven platform for developer insights

DX developer intelligence platform for actionable insights, homepage screenshot.

DX is a developer intelligence platform that uses data and developer surveys to help engineering teams improve productivity. DX aggregates activity from code, CI/CD, and project management to identify bottlenecks. Customers report improvements of up to 6 times faster lead times and 2 times higher deployment rates.

Key features and benefits:

  • Centralized engineering data

  • Automated surveys and feedback loops for a full view of developer experience

  • Tools to standardize developer workflows

  • AI and automation features

3. Jellyfish: Intelligence for AI-integrated teams

Jellyfish developer tool for complex projects, homepage screenshot.

Jellyfish is a software engineering intelligence platform that collects data from repositories and issue trackers to give leaders clear visibility into productivity. Its dashboards display metrics like cycle time, code churn, and AI tool usage to spot bottlenecks early.

Key features and benefits:

  • Unified dashboards for Git, Jira, CI/CD, and other sources

  • Capacity planning to balance team workloads

  • Measurement tools for AI impact and team morale

  • Jira integration to turn comments into issues

4. Linearb: Workflow optimization

LinearB, one of the best tools to improve developer productivity, homepage screenshot.

LinearB is an engineering productivity platform that provides visibility into developer workflows, automation, and process metrics. It collects data across the entire development lifecycle to diagnose blockers and optimize delivery. One user reports saving 321 developer-hours per month.

Key features and benefits:

  • Real-time DORA and SPACE metrics visualized in dashboards.

  • Rule-based bots to auto-assign PRs, manage labels, block large PRs.

  • Built-in team health checks and surveys.

  • AI tool integrations.

  • Project forecasting module to predict delivery dates based on historical velocity.

5. Harness: AI-driven CI/CD platform

Harness tool for AI powered development, homepage screenshot.

Harness is an AI-driven continuous integration and delivery platform. It offers advanced deployment automation and verification and helps automate deployments with GitOps or push-based workflows, using AI to analyze test results.

Key features and benefits:

  • Runs only impacted tests, reducing pipeline time.

  • Advanced deployment strategies with automated rollback on failure.

  • Real-time metrics on deployment frequency, failure rates, and cost.

6. CodeRabbit: Industry leading AI code review platform

CodeRabbit: Industry leading AI code review platform

CodeRabbit is the category-defining platform for AI-powered code reviews, built for modern engineering teams navigating the rise of AI-generated development. It combines generative AI models with linters and static code analyzers to deliver codebase-aware reviews that catch and fix errors before they reach production. By pulling in dozens of contextual signals, CodeRabbit provides comprehensive, context-aware reviews, along with powerful customization features that tailor feedback to your codebase and reduce noise. 

Key features and benefits:

  • Codebase-aware AI reviews: Reviews PRs in the context of your entire codebase to catch regressions and side effects diff-only reviews miss.

  • Actionable, high-signal feedback: Flags bugs, logic errors, security issues, test gaps, and docstring drift without drowning you in noise.

  • Faster reviews and merges: PR summaries, walkthroughs, and one-click fixes reduce back-and-forth and help teams merge significantly faster.

  • Fits into your workflow: Works directly inside your Git platform and supports IDEs, acting as a first-pass reviewer so humans can focus on intent and architecture.

7. Axify: Software delivery intelligence

Axify developer productivity tool homepage screenshot.

Axify is a metrics and forecasting tool designed to help development teams deliver faster. It tracks DORA metrics, flow metrics, and team well-being to optimize development processes. Axify can forecast delivery dates based on historical data and current sprint progress, alerting teams to risks early on.

Key features and benefits:

  • Predicts release dates and highlights overdue work based on past performance.

  • Monitors lead time, deployment frequency, cycle efficiency, and other key metrics.

  • Visualizes the software delivery process to pinpoint delays and waste.

  • Surveys to assess developer sentiment.

  • Progress tracking through integrated metrics.

8. Atlassian Compass: Modern developer portal for small teams

Atlassian Compass, one of the best dev tools, homepage screenshot.

Atlassian Compass is a free internal developer portal for cataloging services, components and templates. It provides a component catalog and health scorecards so teams can easily discover and monitor all their software services. Compass also lets organizations codify best practices via golden path templates so new components start with secure, observable defaults.

Key features and benefits:

  • Inventory of all code components with metadata, dependencies, docs, and owner info.

  • Health scorecards.

  • Upstream and downstream service mapping for impact analysis.

  • Reusable templates for project creation that enforce policies automatically.

9. Claude Code: AI coding assistant for developer productivity

Homepage screenshot of Claude Code software developer productivity tool for maximum coding efficiency.

Claude Code is a coding assistant that runs in the developer’s terminal. You describe what you want in plain English, and Claude Code will generate code, fix bugs, or automate tasks. It can operate with deep context or integrate with external tools.

Key features and benefits:

  • Easy code generation with Claude making an execution plan and coding it.

  • Automated debugging and fix implementation.

  • Codebase Q&A.

  • Operates via CLI.

10. Cursor: Advanced AI code editor

Cursor productivity tool for intelligent assistance homepage screenshot.

Cursor is an AI-powered code editor that uses Claude, GPT, and Gemini models to suggest whole-line or multi-line completions as you type, often just by hitting Tab. Cursor also lets you use natural language commands to modify code, applying them across files.

Key features and benefits:

  • Context-aware autocomplete that predicts code edits and fills them in bulk.

  • Codebase awareness.

  • The option to disable cloud logging so your code is not stored remotely.

  • Supports many AI models with custom agent layers for speed.

11. Copilot: AI coding assistant

Copilot development tool with code extensions for all major editors, homepage screenshot.

GitHub Copilot is an AI coding assistant integrated into IDEs that suggests code completions, functions, and generates code from comments as you type. Copilot uses OpenAI and GitHub models to offer context-aware recommendations.

Key features and benefits:

  • Predicts and inserts code snippets and functions based on surrounding code and comments.

  • Chat interface for code Q&A and generation.

  • Extensions for all major editors.

  • Warns about code vulnerabilities and insecure patterns in.

12. Gradle: Build automation and dependency management

Dependency management tool Gradle homepage screenshot.

Gradle is a modern build system and dependency manager used in Java, Kotlin and Android development. It uses a task-based approach and supports incremental builds and caching for speed.

Key features and benefits:

  • Incremental builds

  • Shares build results across developers and CI to avoid redundant work

  • Builds native code, Android apps, and Node.js projects

  • Thousands of plugins

  • Highly extensible and type-safe

13. Postman: The best API development platform

Postman API development platform homepage screenshot.

Postman is an API development platform that helps teams design, test, and document APIs collaboratively. Teams use Postman collections and workspaces to share endpoints and scripts for consistency across projects. This helps teams onboard faster and reduce errors.

Key features and benefits:

  • Collaboration workspaces with shared collections, mock servers, and docs

  • Auto-generated, interactive docs for all endpoints

  • Automation with AI assistant to write test scripts or debug APIs

  • Links with version control, CI/CD, and cloud tools to embed APIs into the lifecycle

14. GitHub Actions: Native CI/CD and automation

Native CI/CD automation platform Github Actions homepage screenshot.

GitHub Actions lets developers automate workflows directly within GitHub. You write YAML workflow files that trigger on repository events to build, test, and deploy code. Actions provides hosted runners and supports matrix builds, so you can test across multiple OS versions in parallel.

Key features and benefits:

  • Runs builds and deployments in your GitHub repo context

  • Simultaneous code testing on multiple operating systems and language versions

  • Real-time workflow logs and an encrypted secrets store for credentials.

  • Workflow automation

15. Crucible: Collaborative code review tools

Screenshot of Crubicle collaborative code review tool for multiple team members.

Crucible is a code review tool that teams perform peer reviews on commits from Git, SVN, and Mercurial with discussion threads on specific lines or files. It supports both formal workflows and informal reviews alike, and provides dashboards to track review status and code coverage.

Key features and benefits:

  • Inline comments, threaded discussions, and review statuses to collaborate on code quality

  • Tracking and reporting

  • Connects to Jira and Bitbucket Server

  • Complete history of reviews and comments for compliance and accountability.

Choosing the right developer productivity tool

Developer productivity is about creating the right environment for developers to write code, instead of wasting their time and cognitive energy on dealing with inefficiencies and bottlenecks.

The 15 tools we’ve explored here stand out as some of the most powerful resources today, covering everything from data-driven insights and automated deployments to intelligent code suggestions and quality checks.

Mutation Testing as Architecture Enforcement: Infection in 2026

Mutation testing flow: source code is mutated into variants, the test suite runs against each, and each mutant is killed or escapes

Picture the shape of bug that escapes a green pipeline. A team ships a refactor of a pricing module. The coverage report says 94%. CI is green. Days after release, a customer gets charged the wrong amount on a multi-pack discount that was supposed to drop the total by a clean percentage.

Then someone pulls up the test that was supposed to cover the discount math, and it ends like this:

public function testDiscountAppliesToMultiPack(): void
{
    $cart = new Cart();
    $cart->addItem(new Item('sku-1', 3, 1000));
    $price = $this->pricer->price($cart);

    $this->assertNotNull($price);
}

The test ran the code. Coverage counted the lines. The assertion checked that the result was not null. Nothing checked the actual number. Months of changes had slipped past it.

Coverage tells you which lines ran. It does not tell you which lines were verified. Mutation testing closes that gap. It changes your code in small, deliberate ways and reruns the tests. Anything that still passes is a test that wasn’t really testing.

Infection on PHP 8.3, a real surviving mutant, and the reason mutation testing isn’t a vanity metric.

What mutation testing actually does

Infection reads your source, applies small code mutations (a > becomes >=, a + becomes -, a return $value becomes return null), and runs your test suite against each mutated version. There are three possible outcomes per mutation:

  • Killed. At least one test failed. Good — your tests noticed the bug.
  • Escaped. All tests passed against the mutated code. Bad — your tests are blind to that behavior.
  • Timed out / errored. The mutation broke the program in some other way. Treated as killed.

The headline number is the Mutation Score Indicator (MSI): the percentage of mutations killed. A second number, Covered Code MSI, restricts the denominator to lines your tests actually executed, so it isolates “tests that look at the code but do not really test it” from “tests that do not touch the code at all.”

The two numbers tell different stories. Low coverage MSI means whole files are untested. If covered MSI is low, the tests that exist are weak. The second one is what bites you in production, because it hides under a green coverage badge.

Mutation testing flow: source code is mutated into variants, the test suite runs against each, and each mutant is killed or escapes

Setting up Infection on PHP 8.3

Install it as a dev dependency. Infection 0.29+ works with PHP 8.2 and 8.3, and pairs cleanly with PHPUnit 10/11.

composer require --dev infection/infection:^0.29

Drop a configuration file at the project root:

{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": [
            "src/Domain",
            "src/Application"
        ]
    },
    "timeout": 10,
    "logs": {
        "text": "var/infection.log",
        "html": "var/infection.html",
        "summary": "var/infection-summary.log"
    },
    "mutators": {
        "@default": true
    },
    "testFramework": "phpunit",
    "minMsi": 75,
    "minCoveredMsi": 90
}

Three things in this config matter for how you read the result later.

The source.directories block is the architecture lever. You point Infection at the layers where business behavior lives (Domain and Application in a hexagonal layout). You do not mutate Infrastructure, controllers, or framework glue. Mutating an Eloquent model’s $fillable array or a Symfony controller’s response code produces noise. The domain is the part worth testing hard, and Infection’s source filter is how you encode that intent.

The minMsi and minCoveredMsi thresholds are the CI gate. If the score drops below the threshold, Infection exits non-zero. You wire that into your pipeline the same way you wire PHPStan or CS-Fixer.

Run it:

vendor/bin/infection --threads=8 --show-mutations

The --threads flag matters more than people think. Mutation runs are embarrassingly parallel and the wall-clock cost without threading is what makes teams write mutation testing off as “too slow.” On a domain folder of a few thousand lines, eight threads brings a full run under two minutes on a modern laptop.

A surviving mutant: what the report actually shows you

Here is a small domain class. It calculates a tiered discount on a cart subtotal.

<?php
declare(strict_types=1);

namespace AppDomainPricing;

final class TieredDiscount
{
    public function applyTo(int $subtotalCents): int
    {
        if ($subtotalCents > 10_000) {
            return (int) ($subtotalCents * 0.85);
        }

        if ($subtotalCents > 5_000) {
            return (int) ($subtotalCents * 0.95);
        }

        return $subtotalCents;
    }
}

The accompanying test, with the kind of assertions a hurried code review will wave through:

<?php
declare(strict_types=1);

namespace AppTestsDomainPricing;

use AppDomainPricingTieredDiscount;
use PHPUnitFrameworkTestCase;

final class TieredDiscountTest extends TestCase
{
    public function testAppliesADiscount(): void
    {
        $discount = new TieredDiscount();

        $result = $discount->applyTo(20_000);

        $this->assertLessThan(20_000, $result);
    }

    public function testReturnsAnInteger(): void
    {
        $discount = new TieredDiscount();

        $result = $discount->applyTo(7_500);

        $this->assertIsInt($result);
    }
}

PHPUnit reports 100% line coverage for TieredDiscount. Every branch executes at least once across the two tests.

Run Infection:

Mutation Score Indicator (MSI):       75%
Mutations:                            8
Killed:                               6
Escaped:                              2
Errors:                               0
Timeouts:                             0

Two mutants escaped. Pull up the log:

1) src/Domain/Pricing/TieredDiscount.php:11    [M] GreaterThan

--- Original
+++ New
- if ($subtotalCents > 10_000) {
+ if ($subtotalCents >= 10_000) {

Infection changed > to >=. The boundary at exactly 10,000 cents now falls into the 15% discount tier instead of the 5% tier. Neither test passes anything close to 10,000 cents (one uses 20,000, the other 7,500), so the change goes unnoticed. The boundary is silent.

2) src/Domain/Pricing/TieredDiscount.php:15    [M] FloatNegation

--- Original
+++ New
- return (int) ($subtotalCents * 0.95);
+ return (int) ($subtotalCents * -0.95);

The discount factor flipped to negative. A 7,500-cent subtotal now returns -7,125 cents, a refund disguised as a charge. The test asserts the result is an integer. -7,125 is an integer. The mutant survives.

Two escaped mutants. Two real bugs that production would have caught for you, with customers as your test fixtures.

The fix is to write the tests the assertions were pretending to be:

public function testTopTierDiscount(): void
{
    $discount = new TieredDiscount();

    $this->assertSame(17_000, $discount->applyTo(20_000));
}

public function testMidTierDiscount(): void
{
    $discount = new TieredDiscount();

    $this->assertSame(7_125, $discount->applyTo(7_500));
}

public function testTopTierBoundaryExclusive(): void
{
    $discount = new TieredDiscount();

    $this->assertSame(9_500, $discount->applyTo(10_000));
}

public function testMidTierBoundaryExclusive(): void
{
    $discount = new TieredDiscount();

    $this->assertSame(5_000, $discount->applyTo(5_000));
}

public function testNoDiscountBelowThreshold(): void
{
    $discount = new TieredDiscount();

    $this->assertSame(3_000, $discount->applyTo(3_000));
}

Rerun Infection. MSI: 100%. The two mutants that escaped before now die on the boundary tests and the exact-value assertions.

The old tests asked “did something happen?” The new tests ask “what is the answer?” Mutation testing forces that shift in question without you having to enforce it by hand in code review.

Why this belongs in the architecture conversation

A hexagonal codebase puts business behavior in one place and infrastructure in another. The split exists so the parts that encode the rules of the business (the parts the company actually pays for) are testable without a database, without a framework boot, without any of the slow stuff. That is the architectural promise.

Mutation testing is how you verify the promise was kept.

If you point Infection at src/Domain and the MSI is 90%, your domain tests are doing real work. If you point it at the same folder and the MSI is 40%, the architecture is a fiction. The code is structured for testability, and nobody is actually testing it.

This is why the source filter in the Infection config is not a performance tweak. It is a statement about what the codebase is for. The domain is where mutation testing earns its cost. Adapters get integration tests with real Postgres, real HTTP, real queues. Those tests run end-to-end, and a mutation framework adds little signal on top. The domain is the place where pure logic concentrates, and the only place where mutation testing is the right tool.

A pragmatic CI setup encodes that:

- name: PHPUnit
  run: vendor/bin/phpunit

- name: Infection (domain layer)
  run: vendor/bin/infection
       --threads=8
       --min-msi=85
       --min-covered-msi=95
       --only-covered

CLI flags override the JSON config when both are set, so the CI run lifts the thresholds above the local baseline.

Two gates. The first one says the tests must pass. The second one says the tests must mean something. A pull request that adds a new domain class without tests does not regress coverage if the file is empty of behavior, but it regresses MSI, because Infection sees an unverified line and counts it. MSI catches the empty-shell class; coverage doesn’t.

A coverage badge fades while a mutation score takes its place. Coverage shows what ran; mutation shows what was tested

What to skip and what to tune

Three pitfalls catch teams adopting Infection for the first time.

Mutating the wrong layer. If you point Infection at the whole project, you get hundreds of escaped mutants in infrastructure code: Doctrine entity mappings, HTTP response codes, container configuration. They are noise. The mutator does not know that a switched route status is meaningless in your test setup, and you do not want it to. Keep the source filter tight on domain and application.

Treating MSI as a vanity metric. A perfect 100% MSI on a thousand-line domain is achievable, expensive, and the marginal mutant rarely surfaces real bugs. The honest target for production code is around 85–90% on the domain, with the gate set at the level you actually maintain. Teams that chase 100% end up writing tests that assert the mutator can’t reach a branch, not tests that catch bugs.

Running it on every commit. A full Infection run on a large domain takes minutes, not seconds. Run PHPUnit on every commit, Infection on every merge to main, and a full diff-mode run (--git-diff-filter=A, mutating only changed files) on PRs. That is the trade-off most teams settle on after a quarter of usage.

The infrastructure for tracking the score over time exists in the tool. The HTML report shows trends, escaped-mutant categories, and the specific lines that lose points. The escaped-mutant list is the new code-review prompt: instead of “are there enough tests?” the question becomes “why did this mutant escape?”

If this was useful

The hex layout in Decoupled PHP is what makes mutation testing tractable in the first place: a domain you can mutate without booting Laravel or Symfony, a port boundary that tells Infection where to stop, and a use-case layer thin enough that every escaped mutant points at a real gap. The book walks through the testing chapters in the same order this post does: contract tests at the ports, mutation tests in the center, end-to-end on the adapters.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

RabbitMQ vs Kafka: Choosing the Right Messaging System for Real Backend Architectures (part-2)

morpheus-vera profile

This is my part-2 of the topic, in case you would like to go beyond basics of RabbitMQ and Kafka have look at my part-1.

Let’s dive right into the article.

5. Retry Handling, DLQs & Failure Scenarios
Failures are inevitable in distributed systems.

The important question is not:

“Will failures happen?”

The real question is:

“How does the system behave when failures happen repeatedly under load?”

This is where retry strategies, dead-letter queues, and failure handling become critical.

Poor retry design can take down systems faster than the original failure itself.

Retries Are Necessary — But Dangerous

Retries are usually introduced with good intentions:

  • transient network failures,
  • temporary database outages,
  • downstream service timeouts.

But retries also amplify load.

A slow downstream service can quickly become overwhelmed when:

  • hundreds of consumers,
  • retry aggressively,
  • at the same time.

This creates retry storms.

I’ve seen systems where:

  • one slow dependency,
  • triggered queue buildup,
  • which triggered aggressive retries,
  • which eventually exhausted thread pools,
  • database connections, and
  • CPU across multiple services.

The original issue was small.

The retry strategy made it catastrophic.

RabbitMQ Retry Patterns

RabbitMQ provides flexible retry handling using:

  • acknowledgments,
  • dead-letter exchanges,
  • delayed queues, and
  • TTL-based routing.

A common production pattern looks like this:

  • Consumer processing fails
  • Message moves to retry queue
  • Retry queue delays processing
  • Message returns to main queue
  • After max retries, move to DLQ

This approach gives strong operational control.

RabbitMQ is particularly good at workflow-oriented retry management because routing behavior is broker-driven.

That flexibility is one reason RabbitMQ remains popular for transactional systems.

Kafka Retry Patterns

Kafka handles retries differently.

Since messages remain in the log:

  • retries are often implemented at the consumer layer,
  • not at the broker layer.

Common approaches include:

  • retry topics,
  • delayed retry topics,
  • parking-lot topics, and
  • consumer-side retry orchestration.

This model gives flexibility at scale, but introduces more architectural responsibility.

Teams often underestimate the complexity of retry orchestration in Kafka systems.

Especially when:

  • ordering matters,
  • failures are partial, and
  • consumers operate at high throughput.

Dead-Letter Queues (DLQs)

Not every message should be retried forever.

Some messages are fundamentally invalid:

  • corrupted payloads,
  • schema mismatches,
  • business rule violations,
  • malformed events.

These are poison messages.

Without DLQs, these messages can repeatedly fail and block processing indefinitely.

A DLQ acts as an isolation zone for failed messages.

This allows engineers to:

  • inspect failures,
  • replay selectively,
  • debug safely, and
  • avoid endless retry loops.

A production system without DLQs is usually incomplete.

Failure Recovery Is an Architectural Concern

One of the biggest misconceptions in messaging systems is:

“The broker handles reliability.”

Not entirely.

Reliable systems come from:

  • idempotent consumers,
  • controlled retries,
  • failure isolation,
  • observability, and
  • safe recovery workflows.

Messaging platforms help.

But application design still determines system resilience.

6. Replayability & Event Retention

One of Kafka’s biggest strengths is replayability.

And this is where Kafka fundamentally separates itself from traditional messaging systems.

RabbitMQ Message Lifecycle

RabbitMQ is optimized for message delivery.

Once a message is:

  • consumed,
  • acknowledged,
  • and removed

its lifecycle is effectively complete.

That works perfectly for:

  • background jobs,
  • async workflows,
  • task execution,
  • transactional processing.

Most workflow systems care about:

“Was the task completed successfully?”

Not:

“Can we replay this event history later?”

RabbitMQ prioritizes delivery flow over long-term event retention.

Kafka Event Retention Model

Kafka treats events differently.

Messages are retained for a configurable duration regardless of consumption.

Consumers can:

  • replay old events,
  • restart processing,
  • rebuild projections, or
  • bootstrap new downstream services.

This changes how systems recover from failures.

For example:

  • a downstream analytics service crashes,
  • consumer offsets are reset,
  • historical events are replayed,
  • the system rebuilds state.

No producer changes required.

That capability is extremely powerful in distributed systems.

Why Replayability Matters

Replayability becomes valuable when:

  • systems evolve,
  • new consumers are introduced,
  • historical reconstruction is required, or
  • downstream processing fails.

This is especially common in:

  • event sourcing,
  • audit systems,
  • financial systems,
  • analytics platforms, and
  • CDC pipelines.

In these domains, events themselves become long-term assets.

Kafka was designed for this model.

The Tradeoff

Replayability also introduces operational responsibilities:

  • storage management,
  • retention policies,
  • partition scaling, and
  • consumer offset management.

Retaining massive event histories is not free.

Many teams adopt Kafka for replayability without truly needing it.

If the business problem only requires:

  • reliable task processing,
  • retries, and
  • workflow orchestration,

RabbitMQ is often operationally simpler.

Replayability is powerful.

But unnecessary replayability can become expensive complexity.

7. Operational Complexity

This is the part many comparison articles ignore.

Choosing a messaging system is not only an architectural decision.

It is also an operational commitment.

The complexity you introduce today becomes the operational burden your team manages later.

RabbitMQ Operational Experience

RabbitMQ is generally easier to operate for small-to-medium scale systems.

Its operational model is relatively straightforward:

  • queues,
  • exchanges,
  • bindings,
  • consumers.

Teams can usually:

  • onboard quickly,
  • debug issues faster, and
  • reason about message flow more easily.

For workflow-oriented systems, RabbitMQ often feels operationally intuitive.

This simplicity matters more than many teams realize.

Especially for smaller engineering organizations.

Kafka Operational Reality

Kafka introduces a different level of operational complexity.

At scale, teams must think about:

  • partition strategy,
  • broker balancing,
  • consumer lag,
  • rebalancing behavior,
  • retention policies,
  • storage growth,
  • replication,
  • throughput tuning, and
  • cluster sizing.

Most Kafka problems are not coding problems.

They are operational scaling problems.

For example:

  • poorly chosen partition counts,
  • uneven partition distribution,
  • slow consumers,
  • large retention windows

can create production issues that are difficult to diagnose later.

Kafka is incredibly powerful, but that power comes with operational responsibility.

Consumer Lag Becomes a Core Metric

In Kafka systems, consumer lag becomes one of the most important operational indicators.

Lag represents:

how far consumers are behind producers.

High lag usually signals:

  • slow downstream systems,
  • processing bottlenecks,
  • scaling issues, or
  • unhealthy consumers.

Lag accumulation is often gradual.

By the time users notice failures, the backlog may already be massive.

Operational visibility becomes essential.

Simplicity Is Often Undervalued

One pattern I’ve seen repeatedly:

  • teams adopt Kafka because “large companies use Kafka,”
  • but their actual workload only requires reliable asynchronous processing.

In many such cases:

  • RabbitMQ would have been simpler,
  • cheaper to operate, and
  • easier to maintain.

Distributed systems are already complex.

Introducing operational complexity without clear architectural need rarely ends well.

The best engineering decisions are not always the most technically impressive ones.

Often, they are the systems that remain understandable and maintainable under production pressure.

8. Real-World Use Cases

This is where attending many meetups and conferences helped shape my understanding.

In production systems, messaging platforms are rarely chosen because of individual features.

They are chosen because of:

  • workload characteristics,
  • operational expectations,
  • scalability requirements, and
  • failure recovery needs.

This is where RabbitMQ and Kafka naturally separate into different strengths.

E-Commerce Order Processing

Let’s take an example of any E-Commerce platforms’ order processing. Consider a typical order workflow:

  • order placed,
  • payment processed,
  • inventory reserved,
  • invoice generated,
  • notification sent.

These are transactional workflows with multiple dependent steps.

The primary concern here is usually:

  • reliable task execution,
  • retry handling,
  • workflow routing, and
  • operational visibility.

RabbitMQ fits naturally in this model.

Its routing flexibility and acknowledgment-based delivery make workflow orchestration relatively straightforward.

For example:

  • failed payments can move into retry queues,
  • notification failures can be isolated separately, and
  • dead-letter queues can capture permanently failed events.

In these systems, replaying six months of historical order events is rarely the primary requirement. Reliable processing is.

Payment Processing Systems

Payment systems introduce another level of reliability requirements.

A payment event may involve:

  • fraud validation,
  • balance checks,
  • third-party gateways,
  • settlement systems, and
  • reconciliation workflows.

Failures must be controlled carefully.

Infinite retries can become dangerous very quickly.

For example:

  • duplicate payment processing,
  • repeated external API calls, or
  • accidental financial side effects.

RabbitMQ is commonly used in such systems because:

  • retries are easier to control,
  • routing behavior is flexible, and
  • workflow visibility remains operationally manageable.

That being said, many financial systems also use Kafka for:

  • audit trails,
  • event streaming,
  • fraud analytics, and
  • transaction history pipelines.

This is where hybrid architectures often emerge naturally.

Notification Systems

Notification systems usually involve:

  • email delivery,
  • SMS processing,
  • push notifications,
  • webhook dispatching.

These workloads are asynchronous by nature.

RabbitMQ works well here because:

  • fanout patterns are simple,
  • retries are operationally manageable, and
  • delayed delivery patterns are easy to implement.

For example:

  • retry email delivery after temporary SMTP failure,
  • isolate failed webhook deliveries,
  • throttle downstream notification providers.

The routing capabilities of RabbitMQ are extremely useful in these scenarios.

Real-Time Analytics

Analytics workloads behave very differently.

Imagine:

  • clickstream ingestion,
  • application telemetry,
  • IoT event streams,
  • user activity tracking.

Now the problem shifts toward:

  • massive throughput,
  • durable event retention,
  • horizontal scaling, and
  • replayability.

Kafka becomes significantly stronger here.

Its partitioned append-only log architecture allows:

  • high ingestion throughput,
  • parallel consumer processing,
  • long-term event retention, and
  • downstream replay capabilities.

This is where Kafka dominates:

  • analytics pipelines,
  • observability systems,
  • stream processing, and
  • telemetry platforms.

In these systems, events themselves are valuable long after initial processing.

Audit & Event Sourcing Systems

Some systems require immutable historical event tracking.

Examples include:

  • financial ledgers,
  • compliance systems,
  • user activity auditing,
  • domain event sourcing.

Replayability becomes crucial here.

Kafka’s retention model makes it highly suitable for these architectures.

Consumers can:

  • rebuild projections,
  • replay historical state,
  • bootstrap new systems, or
  • recover corrupted downstream services.

RabbitMQ is not designed for this style of long-lived event retention.

Kafka wins in these scenarios.

When Companies Use Both

Some mature backend architectures eventually adopt both RabbitMQ and Kafka.

A common pattern looks like this:

  • RabbitMQ for transactional workflows and operational messaging
  • Kafka for analytics, event streaming, and long-term event retention

For example:

  • order service publishes workflow tasks through RabbitMQ
  • completed business events stream into Kafka for analytics and downstream consumers

This separation works well because both systems optimize for different concerns.

Trying to force one technology to solve every asynchronous problem often creates unnecessary complexity.

Good architecture is rarely about choosing a single perfect tool.

It is usually about understanding where each tool fits naturally.

Assisted ChatGPT to generate images.

In the next-part of the article, I’d like to include some code examples, common mistakes teams make, and so on.

Notes + Local AI: Simpler Than You Think

I got caught flat-footed twice by the same customer. I have an office hours call with them almost every week. For a couple of weeks in a row they asked the same question: any update on that issue from last month? I’d written it down both times. I just never turned it into a ticket.

Office hours with active customers is some of the best feedback a startup gets. They’re in the product, they know what’s broken, and they’re telling you directly instead of just quietly churning. When they point out you forgot to track their feedback it’s embarrassing. But more importantly, it tells you something’s wrong with the system, not just your memory.

I’ve tried a lot of note-taking approaches. Written by hand, scanned handwritten notes, typed everything up, recorded audio and had it transcribed. Capture was fine. The Follow-up section was where things died.

From 2022 to 2024 I used Notion. Got to 500+ notes. I thought the tagging and grouping would help me actually use what I’d captured. But it was just another black box. Notes went in, nothing came back out. Search worked fine, but only if I went looking, and between back-to-back calls, I wasn’t going looking. And if I wanted to feed those notes to an LLM, I had to export everything, unzip a folder, run through a whole process. Enough friction that I just didn’t.

So last year I exported everything as markdown and wrote a script to add frontmatter to all of them. Title, date, note type, meeting type (customer, vendor, investor, internal), company, tags.

$ ls -al spd-ai-notes | wc -l
    2257

2,200+ files. Any script, any AI tool, any grep can read them directly. No export. No process.

Customer notes

I use four templates: technical, meeting, internal, and finance. The meeting template has four sections: Attendees, Agenda, Notes, Follow-up. I fill in half of it in the minute before the call starts. Customer feedback goes in Notes. Anything I need to act on goes in Follow-up.

---
title: "{{title}}"
date: {{date:YYYY-MM-DD}}
note_type: meeting
meeting_type:
company:
tags:
  -
---
# {{title}}

## Attendees
- 

## Agenda
- 

## Notes
- 

## Follow-up
- 

The Follow-up section is the one that used to get lost. Now an AI agent reads it.

I can point Claude or Qwen or DS4 at the notes folder and say “read my meeting notes from the last week, find follow-up items related to product issues, and create a Linear ticket for each one.” First time I ran it, it made 20 tickets. A full week of calls where customers had mentioned things in passing, I’d written them down, and nothing had happened. One pass, done.

It works the other direction too. Before a call, point the AI at your notes for that customer and ask for a quick brief: what did we cover last time, what’s open, what changed. Fifteen minutes between back-to-back calls isn’t enough to dig through notes manually. One prompt is.

Customer notes also flow into our GTM system so the sales context is there when the next call happens. Follow-ups from meetings feed my weekly TODO. And when I’m working with an AI agent on something and it spots an issue, it can drop a note right into that same folder. I’ll pick it up later.

Finance checklist

The finance template works differently. Instead of capturing what happened, it’s a checklist I run 2-3 times a month: vendor invoices, customer AR aging, bank balance, payroll, board reporting. Every item links directly to the system I need to check. Open the note, work down the list, check things off. Takes about half the time it used to because I’m not trying to remember what I missed last time.

---
title: "{{title}}"
date: {{date:YYYY-MM-DD}}
note_type: internal
topic: finance
participants:
  - 
tags:
  -
---
# {{title}}

## Participants
- 

## Checklist
- [ ] Vendor invoices (AP)
    - [ ] Contractors
    - [ ] SaaS subscriptions
    - [ ] Other vendors
- [ ] Customer invoices (AR aging report)
    - [ ] Outstanding by customer
    - [ ] Past due follow-ups
- [ ] Payment processor
    - [ ] Open invoices
    - [ ] Failed charges
- [ ] Marketplace revenue
    - [ ] Active agreements
    - [ ] Billed revenue
- [ ] Upcoming expenses
    - [ ] Credit card balance
    - [ ] Other payables
- [ ] Confirm bank balance
    - [ ] Payroll clearing
    - [ ] Operating account
- [ ] Bookkeeping review
    - [ ] Recent transactions
    - [ ] Outstanding requests
- [ ] Board/investor reporting
- [ ] Compliance
    - [ ] State and local filings
    - [ ] Audits

Same idea works for quarterly security reviews, board prep, anything repeatable that currently lives in your head.

The wider ecosystem

Because everything is plain text, you’re not locked into any one tool.

For viewing and navigating, Obsidian handles large markdown libraries well: graph view, tag search, template plugins. VSCode works too if you’d rather stay in your dev environment. Both read the same folder with no conversion needed.

For AI, you don’t have to use a cloud model. Ollama and Apple MLX let you run models locally against the same folder. Useful if you have notes you’d rather not send to an external API. DS4 is worth looking at specifically. The latest models support up to 200k token context windows, so you can feed in most of your notes folder in a single pass.

And if you need a different output format, just ask the AI to convert. Board summary, customer brief, HTML email. Markdown converts cleanly to any of them. The notes become inputs to other systems, not just records you write and forget.

The value of an app like Notion is the UI. The cost is that your data only works inside that app. Plain text inverts that tradeoff.

The note-taking advice you usually see is about capturing or summarizing a meeting. That’s not wrong. But capture is the easy part.

The hard part is what happens to a note after it’s written. If the answer is “nothing, unless I manually go find it,” there’s a hole in the system regardless of how good the notes are. Pointing an AI at the folder is what closed it.

That customer still does office hours with me. When they ask about last month’s item, I’ve got a ticket to show them.