<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Honeybadger Developer Blog</title>
  <subtitle>Useful articles for web developers in Ruby, Javascript, Elixir, and more</subtitle>
  <id>https://www.honeybadger.io/blog/</id>
  <link href="https://www.honeybadger.io/blog/"/>
  <link href="https://www.honeybadger.io/blog/feed.xml" rel="self"/>
  <updated>2026-05-28T07:00:00+00:00</updated>
  <author>
    <name>The Honeybadger.io Crew</name>
  </author>
  <entry>
    <title>Next.js vs React: What&#x2019;s the difference and which should you use?</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/next-js-vs-react/"/>
    <id>https://www.honeybadger.io/blog/next-js-vs-react/</id>
    <published>2026-05-28T07:00:00+00:00</published>
    <updated>2026-05-28T07:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">Next.js and React are often compared, but they solve different problems. React focuses on building user interfaces, while Next.js adds structure, rendering strategies, and back-end capabilities. Read this article to see how we break down their differences.</summary>
    <content type="html">&lt;p&gt;The Next.js vs React question is not really a comparison between two competing tools &#x2014; Next.js is built on top of React. React itself is a UI rendering JavaScript library used for building user interfaces across platforms, including web applications and mobile apps with React Native, while Next.js is a framework that wraps React and makes concrete decisions about routing, data fetching, and server-side concerns. Understanding this relationship is the starting point for every project decision you will make when building web applications.&lt;/p&gt;
&lt;p&gt;React handles one job extremely well: taking a component tree and turning it into DOM output, then reconciling changes efficiently. Every other layer like how you fetch data, how you route between pages, what runs on the server versus the client is deliberately left to the developer or to third-party libraries. Next.js packages those decisions into a cohesive framework, adding server-side rendering, file-system-based routing, built-in image optimization, and an API routing layer that runs alongside your JavaScript code. This makes it particularly useful for complex projects that require coordination between the front-end and back-end.&lt;/p&gt;
&lt;p&gt;This article covers what each tool does at a technical level, how they differ in behavior and file structure, how their rendering modes work, and answers the common question: what is Next.js vs React in practical terms?&lt;/p&gt;
&lt;h2&gt;What is React?&lt;/h2&gt;
&lt;p&gt;React&apos;s core contribution to web development is the component-based architecture combined with a virtual DOM diffing algorithm. Before React, updating the DOM in response to state changes meant either re-rendering entire page sections or writing granular imperative update logic that quickly became unmaintainable.&lt;/p&gt;
&lt;p&gt;React introduced a declarative model:&#xa0;describe what the user interface should look like for a given state, and let the reconciler determine the minimum set of real DOM mutations required. This approach transformed web development workflows and made React a widely adopted JavaScript library for building complex user interfaces.&lt;/p&gt;
&lt;h3&gt;The component and hook model&lt;/h3&gt;
&lt;p&gt;A React component is a function that accepts props and returns JSX. The function re-runs whenever its state or props change, and React reconciles the output against the previous virtual DOM snapshot. Hooks like &lt;code&gt;useState&lt;/code&gt; and &lt;code&gt;useEffect&lt;/code&gt; let you attach stateful behavior and side effects to functional components without resorting to class syntax.&lt;/p&gt;
&lt;p&gt;In practice, components are composed hierarchically, where child components receive data and callbacks from their parents. This composition model is what enables React to scale cleanly in complex projects, especially when managing deeply nested user interface trees.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/UserCard.tsx

import { useState, useEffect } from &apos;react&apos;;

interface User {
  id: number;
  name: string;
  email: string;
}
interface UserCardProps {
  userId: number;
}

export function UserCard({ userId }: UserCardProps) {
  const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    fetch(`/api/users/${userId}`)
      .then((res) =&amp;gt; res.json())
      .then((data) =&amp;gt; {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;;
  if (!user) return &amp;lt;div&amp;gt;User not found&amp;lt;/div&amp;gt;;

  return (
    &amp;lt;div className=&amp;quot;card&amp;quot;&amp;gt;
      &amp;lt;h2&amp;gt;{user.name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component fetches a user when the &lt;code&gt;userId&lt;/code&gt; prop changes, tracks the loading state, and renders conditionally based on that state. The &lt;code&gt;useEffect&lt;/code&gt; dependency array &lt;code&gt;[userId]&lt;/code&gt; ensures the fetch only re-runs when &lt;code&gt;userId&lt;/code&gt; changes, not on every render. This pattern is idiomatic React, but notice that the data fetch happens entirely in the browser after the component mounts. There is no concept of running this on a server.&lt;/p&gt;
&lt;h3&gt;What React deliberately leaves out&lt;/h3&gt;
&lt;p&gt;React provides no routing system. Navigation between views requires installing a library like React Router or TanStack Router. React also has no built-in data fetching convention, no server-side rendering pipeline, no image optimization, and no API routes server. You can combine React with Express for server-side rendering and with third-party libraries like SWR or TanStack Query for caching, but you must assemble these pieces yourself across multiple JavaScript files.&lt;/p&gt;
&lt;p&gt;This is not a weakness. For applications that run entirely in the browser, including dashboards and progressive web apps, the absence of framework opinions means less configuration overhead and more flexibility in your dependency choices. The cost is that you own the architecture decisions.&lt;/p&gt;
&lt;p&gt;React also benefits from a large and active community, which means most problems you encounter already have established patterns or libraries.&lt;/p&gt;
&lt;h2&gt;What is Next.js and how does it extend React?&lt;/h2&gt;
&lt;p&gt;Next.js extends the React component model with conventions and runtime capabilities that React itself does not provide. The two most significant additions are the rendering pipeline (Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR)) and the file-based routing system. Everything else&#x2014;like API handling, image optimization, the Edge Runtime, font loading, and middleware&#x2014;builds on top of those two foundations.&lt;/p&gt;
&lt;h3&gt;The App Router and Server Components&lt;/h3&gt;
&lt;p&gt;Next.js 13 introduced the App Router, which changed the default rendering model. In the App Router, every component is a React Server Component (RSC) unless you explicitly opt out with the &lt;code&gt;&apos;use client&apos;&lt;/code&gt; directive.&lt;/p&gt;
&lt;p&gt;Server Components render on the server, can &lt;a href=&quot;https://www.honeybadger.io/blog/javascript-concurrency/&quot;&gt;await async operations&lt;/a&gt; directly in the component body, and never ship their JavaScript code or the data-fetching JavaScript libraries they use to the client bundle. This results in automatic code splitting, where only the required interactive parts of the application are sent to the browser.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/users/[id]/page.tsx - runs on the server, zero client JS

interface PageProps {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
}

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`, {
    next: { revalidate: 60 }, // ISR: revalidate this data every 60 seconds
  });
  if (!res.ok) throw new Error(&apos;Failed to fetch user&apos;);
  return res.json();
}

export default async function UserPage({ params }: PageProps) {
  const { id } = await params;
  const user = await getUser(id);
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fundamentally different from the React example above. The component is declared &lt;code&gt;async&lt;/code&gt; and awaits the data directly, without &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useState&lt;/code&gt;, or state management libraries. The fetch happens at request time on the server. The client receives pre-rendered HTML. The &lt;code&gt;next: { revalidate: 60 }&lt;/code&gt; option activates Incremental Static Regeneration, so after the initial render, Next.js regenerates the page in the background when a request arrives after 60 seconds, serving the stale version until the fresh one is ready.&lt;/p&gt;
&lt;h3&gt;Client Components and the &apos;use client&apos; boundary&lt;/h3&gt;
&lt;p&gt;When a component needs interactivity, you add &lt;code&gt;&apos;use client&apos;&lt;/code&gt; at the top of the file. This converts the component to a Client Component, which is hydrated in the browser. The boundary between Server and Client Components is explicit and composable: a Server Component can render interactive child components, but a Client Component cannot render a Server Component directly. This separation enables granular automatic code splitting and reduces bundle size.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/components/AddToCartButton.tsx
&apos;use client&apos;;

import { useState } from &apos;react&apos;;

interface AddToCartButtonProps {
  productId: string;
  price: number;
}

export function AddToCartButton({ productId, price }: AddToCartButtonProps) {
  const [added, setAdded] = useState(false);

  async function handleClick() {
    await fetch(&apos;/api/cart&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify({ productId, quantity: 1 }),
      headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
    });
    setAdded(true);
  }

  return (
    &amp;lt;button onClick={handleClick} disabled={added}&amp;gt;
      {added ? &apos;Added to cart&apos; : `Add to cart- $${price}`}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The product page itself (a Server Component) fetches product data on the server and renders HTML, while interactive child components handle user interactions like adding items to a cart. This pattern keeps user interfaces fast and lightweight. This granular control over the client bundle size is one of the most architecturally significant advantages Next.js provides over a pure React SPA (Single Page Application).&lt;/p&gt;
&lt;h2&gt;Key differences&lt;/h2&gt;
&lt;p&gt;The divergence between Next.js and a standalone React application becomes concrete when you compare how each handles routing, the rendering pipeline, project layout, search engine visibility, and runtime performance. These are not surface-level configuration differences; they reflect fundamentally different execution models.&lt;/p&gt;
&lt;h3&gt;Routing&lt;/h3&gt;
&lt;p&gt;React has no router. You install React Router and configure routes manually across multiple JavaScript files. Next.js uses file-based routing. Creating a file automatically registers a route. This removes boilerplate and makes it easier to scale routing in complex projects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/main.tsx - React + React Router v6

import { createBrowserRouter, RouterProvider } from &apos;react-router-dom&apos;;
import { RootLayout } from &apos;./layouts/RootLayout&apos;;
import { HomePage } from &apos;./pages/HomePage&apos;;
import { ProductPage } from &apos;./pages/ProductPage&apos;;
import { NotFound } from &apos;./pages/NotFound&apos;;
import ReactDOM from &apos;react-dom/client&apos;;

const router = createBrowserRouter([
  {
    path: &apos;/&apos;,
    element: &amp;lt;RootLayout /&amp;gt;,
    errorElement: &amp;lt;NotFound /&amp;gt;,
    children: [
      { index: true, element: &amp;lt;HomePage /&amp;gt; },
      { path: &apos;products/:id&apos;, element: &amp;lt;ProductPage /&amp;gt; },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById(&apos;root&apos;)!).render(
  &amp;lt;RouterProvider router={router} /&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next.js uses a file-based routing system. Creating a file at &lt;code&gt;app/products/[id]/page.tsx&lt;/code&gt; automatically registers the route &lt;code&gt;/products/:id&lt;/code&gt;. The folder structure is the route definition. Layouts, loading states, error boundaries, and not-found pages are handled by special files (&lt;code&gt;layout.tsx&lt;/code&gt;, &lt;code&gt;loading.tsx&lt;/code&gt;, &lt;code&gt;error.tsx&lt;/code&gt;, &lt;code&gt;not-found.tsx&lt;/code&gt;) placed in the corresponding route segment directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;app/
&#x251c;&#x2500;&#x2500; layout.tsx          &#x2192; root layout, wraps all routes
&#x251c;&#x2500;&#x2500; page.tsx            &#x2192; renders at /
&#x251c;&#x2500;&#x2500; loading.tsx         &#x2192; Suspense fallback for /
&#x251c;&#x2500;&#x2500; error.tsx           &#x2192; error boundary for /
&#x251c;&#x2500;&#x2500; products/
&#x2502;   &#x251c;&#x2500;&#x2500; page.tsx        &#x2192; renders at /products
&#x2502;   &#x2514;&#x2500;&#x2500; [id]/
&#x2502;       &#x251c;&#x2500;&#x2500; page.tsx    &#x2192; renders at /products/:id
&#x2502;       &#x2514;&#x2500;&#x2500; loading.tsx &#x2192; Suspense fallback for /products/:id
&#x2514;&#x2500;&#x2500; api/
    &#x2514;&#x2500;&#x2500; cart/
        &#x2514;&#x2500;&#x2500; route.ts    &#x2192; API endpoint at /api/cart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file-system router removes an entire category of configuration work. You do not write route declarations, import pages manually, or manage a separate router configuration object. The trade-off is that your folder structure becomes load-bearing.&lt;/p&gt;
&lt;h3&gt;Rendering modes&lt;/h3&gt;
&lt;p&gt;A plain React application renders entirely in the browser. The server sends a mostly empty HTML document with a script tag, and React bootstraps the application in the client. This is client-side rendering (CSR). Next.js supports multiple rendering strategies and enables automatic code splitting at the route and component level. This significantly improves performance for large web applications. Search engines and users on slow connections both receive no meaningful content until the JavaScript parses, executes, and renders.&lt;/p&gt;
&lt;p&gt;Next.js supports four rendering strategies, and you can mix them within a single application. Static site generation renders pages at build time, the HTML is computed once and served as a static file on every request.&lt;/p&gt;
&lt;p&gt;Server-side rendering (SSR) renders on each request, allowing you to fetch fresh data per user or session. Incremental Static Regeneration (ISR) generates the page statically but revalidates it on a configurable interval. Client-side rendering is available for components marked with &lt;code&gt;&apos;use client&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/page.tsx

// Generate static paths at build time (static site generation)
export async function generateStaticParams() {
  const posts = await fetch(&apos;https://api.example.com/posts&apos;).then(r =&amp;gt; r.json());
  return posts.map((post: { slug: string }) =&amp;gt; ({ slug: post.slug }));
}

// Revalidate every 10 minutes (ISR)
export const revalidate = 600;

export default async function BlogPost({ params }: { params: Promise&amp;lt;{ slug: string }&amp;gt; }) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r =&amp;gt; r.json());
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;div dangerouslySetInnerHTML={{ __html: post.content }} /&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;generateStaticParams&lt;/code&gt; function tells Next.js which slugs to pre-render at build time as part of static site generation. Setting &lt;code&gt;revalidate = 600&lt;/code&gt; on the module activates ISR. After ten minutes, the next request triggers a background regeneration. The page still serves instantly from cache while the fresh version is being built. This combination is the standard pattern for content-heavy sites: fast initial load time from static files, with eventual consistency as content changes.&lt;/p&gt;
&lt;h3&gt;SEO implications&lt;/h3&gt;
&lt;p&gt;Search engines index HTML content. A CSR React application sends an empty &lt;code&gt;div&lt;/code&gt;; the content arrives only after JavaScript executes, which introduces indexing uncertainty and delays. Googlebot does execute JavaScript, but not instantly, and social media crawlers (Open Graph, Twitter Cards) typically do not execute JavaScript at all, meaning link previews for a CSR app will be blank.&lt;/p&gt;
&lt;p&gt;Next.js pages rendered via server-side rendering or static site generation (SSG) deliver fully populated HTML on the initial response. You can set metadata like page titles, descriptions, Open Graph tags, and canonical URLs using the built-in Metadata API routes, which generate the correct &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tags at render time on the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/products/[id]/page.tsx

import type { Metadata } from &apos;next&apos;;

interface PageProps {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
}

export async function generateMetadata({ params }: PageProps): Promise&amp;lt;Metadata&amp;gt; {
  const { id } = await params;
  const product = await fetch(`/api/products/${id}`).then(r =&amp;gt; r.json());
  return {
    title: `${product.name} - Acme Store`,
    description: product.description,
    openGraph: {
      title: product.name,
      images: [{ url: product.imageUrl }],
    },
  };
}

export default async function ProductPage({ params }: PageProps) {
  const { id } = await params;
  const product = await fetch(`/api/products/${id}`).then(r =&amp;gt; r.json());
  return &amp;lt;ProductDetail product={product} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the fetch in &lt;code&gt;generateMetadata&lt;/code&gt; and the fetch in the page component both request the same URL. Next.js deduplicates these automatically using the built-in fetch cache. The network request is made once, even though you wrote it twice. This is a concrete example of the framework reducing accidental complexity.&lt;/p&gt;
&lt;p&gt;Quick reference for React vs Next.js:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/next-js-vs-react/next-js-vs-react-comparison.png&quot; alt=&quot;Quick reference for Next.js vs React&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;When to use React on its own&lt;/h2&gt;
&lt;p&gt;Standalone React (scaffolded with Vite or Create React App) is the appropriate choice when your application does not need server-side rendering, has no SEO requirements, and benefits from a thinner dependency footprint. The most common category is authenticated internal tooling: admin dashboards, CRM interfaces, data visualization tools, analytics platforms, and developer consoles. These applications sit behind a login screen, and the route structure is driven by application state (using state management libraries) rather than URL semantics.&lt;/p&gt;
&lt;p&gt;React can also be extended beyond the web using React Native, allowing you to reuse concepts and patterns when building mobile apps.&lt;/p&gt;
&lt;p&gt;A Vite-based React setup produces a minimal project with fast web development server startup and highly optimized production builds via Rollup. There is no server process to manage, no framework conventions to learn, and no build-time rendering pipeline to reason about.&lt;/p&gt;
&lt;p&gt;If your team is familiar with React but not with Next.js-specific concepts like the App Router, Server Components, or the distinction between server-side and client-side data fetching, a plain React setup removes that cognitive surface area.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Scaffold a React + TypeScript app with Vite
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

# Install Router for client-side navigation
npm install react-router-dom

# Install TanStack Query for data fetching and caching
npm install @tanstack/react-query
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This combination of Vite, React Router, and TanStack Query covers the needs of most internal web applications. TanStack Query handles caching, background refetching, loading, and error states, among other things&#x2014;with considerably less boilerplate than manually managing state with &lt;code&gt;useEffect&lt;/code&gt;. For web applications where all data fetching is client-side anyway, this stack is competitive with Next.js in terms of developer experience.&lt;/p&gt;
&lt;h3&gt;Integrating React into an existing back-end&lt;/h3&gt;
&lt;p&gt;Another valid use of standalone React is when you have an existing server-side framework like Rails, Django, Laravel, or Spring, and you want to embed React components into specific pages rather than migrate to a JavaScript-first stack.&lt;/p&gt;
&lt;p&gt;Third-party tools like Inertia.js let Rails or Laravel applications render React components server-side and manage navigation without a full SPA migration. In this architecture, Next.js would be redundant: the host framework already handles routing and server rendering.&lt;/p&gt;
&lt;h2&gt;When Next.js makes more sense&lt;/h2&gt;
&lt;p&gt;Next.js makes more sense when your application needs server-side rendering (SSR) for SEO or performance, when you want to colocate your API routes with your front-end code, or when you are building a content-heavy site where static site generation with revalidation provides both performance and freshness.&lt;/p&gt;
&lt;h3&gt;Public-facing applications with SEO requirements&lt;/h3&gt;
&lt;p&gt;Marketing sites, e-commerce websites, documentation, blogs, SaaS landing pages, and any application where pages need to rank in search results are natural fits for Next.js. Unlike React, the ability to combine static generation for high-traffic stable pages with ISR for frequently changing content, and server-side rendering (SSR) for personalized or session-dependent pages&#x2014;all within a single codebase&#x2014;is architecturally easy to implement.&lt;/p&gt;
&lt;p&gt;Consider a product listing page on an e-commerce site. You want the page to load instantly (static or ISR for performance), include accurate metadata for search engines and social sharing (server-side rendering or static site generation (SSG) for SEO), and show personalized pricing or stock information (partial hydration with a Client Component making a client-side fetch after the static shell renders).&lt;/p&gt;
&lt;p&gt;All of this is expressible in Next.js with standard patterns; in a plain React SPA, it requires either a separate server-side rendering (SSR) infrastructure or accepting the SEO limitations of client-side rendering.&lt;/p&gt;
&lt;h3&gt;Full-stack applications without a separate API service&lt;/h3&gt;
&lt;p&gt;Next.js Route Handlers let you define API endpoints that run alongside your front-end code, share type definitions, and deploy together as a single unit. For web development projects where the API and the UI are maintained by the same team and the back-end complexity does not justify a separate service, this is a significant simplification:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/api/orders/route.ts - runs on the server, not in the browser

import { NextRequest, NextResponse } from &apos;next/server&apos;;
import { db } from &apos;@/lib/db&apos;; // direct database access from the API route
import { auth } from &apos;@/lib/auth&apos;;

export async function GET(request: NextRequest) {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: &apos;Unauthorized&apos; }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get(&apos;page&apos;) ?? &apos;1&apos;, 10);
  const limit = 20;

  const orders = await db.order.findMany({
    where: { userId: session.user.id },
    orderBy: { createdAt: &apos;desc&apos; },
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({ orders, page });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This API route has direct database access, it imports a Prisma client, queries the database, and returns JSON. The front-end components in the same codebase can call this endpoint with a simple &lt;code&gt;fetch(&apos;/api/orders&apos;)&lt;/code&gt;. TypeScript types for the response can be shared between the route handler and the calling component without publishing a separate package or running a code generation step. For small to medium teams building full-stack web applications, this tight coupling is a feature, not a liability.&lt;/p&gt;
&lt;h3&gt;Server Actions for form handling and mutations&lt;/h3&gt;
&lt;p&gt;Next.js Server Actions allow you to define server-side mutation functions that can be called directly from Client Components without going through an explicit REST endpoint. This is the pattern to reach for when you need form submissions, data mutations, or any user-triggered server-side operation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/actions/createPost.ts
&apos;use server&apos;;

import { revalidatePath } from &apos;next/cache&apos;;
import { db } from &apos;@/lib/db&apos;;
import { auth } from &apos;@/lib/auth&apos;;

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session) throw new Error(&apos;Unauthenticated&apos;);

  const title = formData.get(&apos;title&apos;) as string;
  const content = formData.get(&apos;content&apos;) as string;

  await db.post.create({
    data: {
      title,
      content,
      authorId: session.user.id,
    },
  });

  revalidatePath(&apos;/blog&apos;); // purge the cached blog listing
}

// app/blog/new/page.tsx
&apos;use client&apos;;

import { createPost } from &apos;@/app/actions/createPost&apos;;

export default function NewPostForm() {
  return (
    &amp;lt;form action={createPost}&amp;gt;
      &amp;lt;input name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Post title&amp;quot; /&amp;gt;
      &amp;lt;textarea name=&amp;quot;content&amp;quot; placeholder=&amp;quot;Write your post...&amp;quot; /&amp;gt;
      &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Publish&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form&apos;s &lt;code&gt;action&lt;/code&gt; prop accepts the &lt;code&gt;createPost&lt;/code&gt; server action directly. When submitted, Next.js serializes the form data and sends it to the server function, which executes in the Node.js runtime with full access to the database and environment variables. The &lt;code&gt;revalidatePath&lt;/code&gt; call purges the Next.js cache for the &lt;code&gt;/blog&lt;/code&gt; route, so the updated post list appears on the next request without a manual cache invalidation step. This entire pattern (form, mutation, cache invalidation) requires no custom API endpoint.&lt;/p&gt;
&lt;h2&gt;Next.js vs React: The architectural decision&lt;/h2&gt;
&lt;p&gt;The separation is cleaner than it might initially appear. React alone handles use cases where the application runs entirely in the browser and focuses on user interfaces through a component-based architecture. It&apos;s ideal for internal tools, authenticated dashboards, and single-page web applications without SEO requirements. This includes building interactive user interfaces for internal tooling, where client-side rendering is perfectly acceptable. Add Next.js when your web application needs to deliver server-rendered HTML for SEO or performance, when you want to run server-side logic alongside your front-end without maintaining a separate service, or when the file-system router and built-in optimizations justify the additional framework layer.&lt;/p&gt;
&lt;p&gt;The practical signal to watch for is the nature of your first meaningful page render. If a blank loading screen is acceptable because the web application sits behind &lt;a href=&quot;https://www.honeybadger.io/blog/javascript-authentication-guide/&quot;&gt;authentication&lt;/a&gt; and your users do not discover it via search, React&apos;s CSR model is okay. If search engine visibility is important, or the site needs to be fast for unauthenticated users on variable network connections, the server-side rendering capabilities of Next.js are important features.&lt;/p&gt;
&lt;p&gt;One area worth monitoring is the Server Components model. The RSC architecture changes how you reason about data fetching and bundle size in ways that are not entirely settled. The mental model of interleaving server and client components, managing cache invalidation with &lt;code&gt;revalidatePath&lt;/code&gt; and &lt;code&gt;revalidateTag&lt;/code&gt;, and understanding which dependencies run only on the server has a meaningful learning curve. For developers new to both React and Next.js, starting with plain React as a JavaScript library and introducing Next.js only when you need server-side rendering or static site generation is a lower-risk path than beginning with the full App Router feature set.&lt;/p&gt;
&lt;p&gt;If you liked this article and want to learn more about JavaScript, join the &lt;a href=&quot;https://www.honeybadger.io/newsletter/javascript/&quot;&gt;Honeybadger newsletter&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>SIEM alerts: everything you need to know</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/siem-alerts/"/>
    <id>https://www.honeybadger.io/blog/siem-alerts/</id>
    <published>2026-05-21T07:00:00+00:00</published>
    <updated>2026-05-21T07:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">SIEM alerts help you detect suspicious behavior before it becomes a breach. But security monitoring can quickly turn into noisy dashboards and missed threats without the right approach. Read this article to learn how to design effective SIEM alerts and implement real-time security monitoring.</summary>
    <content type="html">&lt;p&gt;Let&apos;s walk through setting up SIEM (Security Information and Event Management) alerts to monitor security threats in applications. We will explain what SIEM alerts are, why they&apos;re relevant with regard to application security, and provide practical examples of common alerts a developer could implement. We will show how to configure simple alerts with Honeybadger Insights.&lt;/p&gt;
&lt;h2&gt;What is SIEM?&lt;/h2&gt;
&lt;p&gt;SIEM (Security Information and Event Management) refers to a class of security platforms that aggregate logs and security events from many systems and analyze them to detect threats. Just like in action movies where a thief exploits an unguarded angle, attackers in the cyber world look for weaknesses. A SIEM system works by pulling data from many different sources across the organization, including security system logs, usual and unusual network traffic, and threat intelligence. The SIEM analyzes them in one place to detect suspicious behavior instead of these signals living in silos.&lt;/p&gt;
&lt;p&gt;The main aim of SIEM is to be a correlation engine. It doesn&#x2019;t just collect raw data; it connects the dots using rule-based correlation, statistical analysis, and sometimes machine learning. The SIEM system continuously evaluates events in real time to identify patterns that indicate a real attack, enabling faster threat detection across your environment. Correlation helps prioritize alerts, not necessarily reduce them automatically. What this means is fewer false positives and clearer signals that something genuinely malicious is happening.&lt;/p&gt;
&lt;h2&gt;The importance of SIEM alerts&lt;/h2&gt;
&lt;p&gt;Organizations often run dozens or even hundreds of disconnected security tools where each generates alerts that require attention. Analysts are forced to jump between dashboards and manually investigate events. SIEM system addresses this problem by acting as the central nervous system of security operations. It prioritizes alerts by severity to help analysts immediately focus on incidents that pose the greatest risk.&lt;/p&gt;
&lt;p&gt;The attackers don&apos;t even discriminate based on the size of the organisation. Some do it for the challenge or fun of it. They target organizations of all sizes and take advantage of openings across applications and cloud infrastructure. A SIEM system provides the visibility and context needed to detect these potential threats early, before they cause serious damage. Much like ignoring a staged diversion at the front of a museum and spotting the real break-in at the back, SIEM becomes one of the most powerful defensive tools security teams have.&lt;/p&gt;
&lt;h2&gt;How to respond to SIEM alerts effectively&lt;/h2&gt;
&lt;p&gt;Responding to SIEM alerts effectively determines whether a security incident is quickly contained or allowed to turn into a breach. Alert incident response procedures must be built on preparation, documentation, and practiced workflows. The goal is not just to react, but to respond effectively.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Establish runbooks for common alert types&lt;/strong&gt;: Runbooks define exactly what should happen when a specific alert is triggered. For example, when a credential stuffing alert fires, the runbook should outline verification steps such as checking whether multiple accounts are affected, containment actions like blocking the attacking IP range, and notification requirements, including informing affected users. By standardizing incident responses, runbooks ensure consistency and help less-experienced team members handle incidents correctly without improvising under pressure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Triage alerts immediately upon receipt&lt;/strong&gt;: The first moments after receiving an alert are very important. Spend some time determining whether the alert represents a genuine threat or requires deeper investigation. Review recent similar alerts to see if the event is part of a broader attack pattern. You can also &lt;a href=&quot;https://docs.honeybadger.io/guides/insights/badgerql/&quot;&gt;query the SIEM&lt;/a&gt; using BadgerQL (Honeybadger&apos;s Query Language) for additional context.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Document investigation steps and findings&lt;/strong&gt;: Every alert investigation should leave a record. When alerts turn out to be false, document why they were triggered and whether tuning adjustments can prevent recurrence. When alerts uncover real attacks, record the attack vector, affected systems, and remediation steps taken.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Implement automated containment where safe&lt;/strong&gt;: Certain scenarios, such as IP-based attacks, benefit from automated containment. When an IP triggers credential stuffing alerts, it can be temporarily blocked at the firewall or web application firewall. This allows containment to happen within seconds rather than waiting for manual action. However, automation must be used carefully. Over-automation can disrupt legitimate users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Coordinate incident response across teams&lt;/strong&gt;: Security incidents often span multiple layers, including applications, infrastructure, and databases. The security team may analyze data of the attack method, development teams patch vulnerable code, and security operations teams apply network-level blocks. Clear communication channels are essential. Many organizations rely on dedicated Slack channels or conference bridges to coordinate effectively during active incidents.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SIEM alert examples and types&lt;/h2&gt;
&lt;p&gt;SIEM alerts fall into several categories based on detection method and threat type. Each category serves a distinct purpose in the comprehensive security monitoring efforts to improve the security posture. Some identify known attack patterns, and some detect subtle behavioral deviations and enforce regulations. Below is a list of SIEM alerts covering the top SIEM alerts by threat detection method and also internal and external threat types.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Signature alerts&lt;/strong&gt;: Signature-based alerts match specific patterns in log data, such as SQL injection attempts in HTTP requests or known malware file hashes. These alerts trigger when logs contain exact strings or regular expression matches associated with attacks. For example, detecting &lt;code&gt;&apos; OR &apos;1&apos;=&apos;1&apos;&lt;/code&gt; in query parameters signals a potential SQL injection probe.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Anomaly alerts&lt;/strong&gt;: Anomaly-based alerts establish behavioral baselines and flag deviations. If a user account typically authenticates from New York during business hours, a login from Singapore at 3 AM exceeds normal behavior thresholds. These alerts require sufficient historical data to build accurate profiles.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Threshold alerts&lt;/strong&gt;: Threshold-based alerts trigger when event counts exceed defined limits within time windows. Failed authentication attempts provide a clear example: five failed logins from a single IP address within ten minutes might indicate credential stuffing, while 100 failed logins across different accounts suggest a broader attack.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compliance threat alerts&lt;/strong&gt;: Compliance reporting enforces regulatory requirements and internal policies. PCI-DSS mandates alerts for unauthorized access attempts to cardholder data, while HIPAA requires notification of protected health information access outside normal workflows. Compliance frameworks require alerting and auditing, but tuning is still necessary to avoid alert fatigue.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Configuring alert triggers and correlation rules&lt;/h2&gt;
&lt;p&gt;Alert triggers define the conditions that generate notifications for common SIEM solution alerts. Simple triggers evaluate single log entries, while complex triggers correlate events across time windows and security data sources. Start with high-confidence signatures for known attacks before layering in anomaly detection and behavioral analysis.&lt;/p&gt;
&lt;p&gt;Authentication alerts should trigger on multiple unsuccessful login attempts, successful logins following failed attempts (credential stuffing success), logins from blacklisted IP addresses, and authentications occurring simultaneously from geographically distant locations. Configure these with appropriate thresholds; three failed attempts might be a typo, but fifteen suggests an attack. Time windows matter too; five failures over 24 hours are less significant than five failures in sixty seconds.&lt;/p&gt;
&lt;p&gt;Alerts on the application side monitor for injection attacks, path traversal attempts, and malicious file uploads. Web application firewalls generate logs that SIEM systems ingest and analyze. When requests attempt to access restricted file paths, or when uploaded files contain executable code. These alerts benefit from whitelisting safe patterns to reduce false positives from legitimate applications or user behavior.&lt;/p&gt;
&lt;p&gt;Data access alerts flag unusual database queries, excessive record retrieval, and access to sensitive data outside normal application workflows. A user downloading sensitive data, such as customer records, at 2 AM warrants investigation, even if their credentials authenticate successfully. Configure these alerts to understand normal data access patterns.&lt;/p&gt;
&lt;h2&gt;Reducing false positives through SIEM alerts best practices&lt;/h2&gt;
&lt;p&gt;Reducing false positives is a core part of SIEM system alerts best practices, as excessive noise diminishes trust in alerting systems. Tuning requires iterative refinement based on investigation outcomes and environmental knowledge.&lt;/p&gt;
&lt;p&gt;Whitelist known-safe activities that trigger alerts. Automated security scanners, monitoring systems, and internal tools often generate traffic patterns resembling attacks. Document these sources and exclude them from triggering alerts. For example, vulnerability scanners probe for SQL injection vulnerabilities as part of routine testing; their IP addresses should be whitelisted to prevent false alarms during scheduled scans.&lt;/p&gt;
&lt;p&gt;Context development could add business intelligence to raw security events. An alert showing &amp;quot;100 failed login attempts from 203.0.113.45&amp;quot; provides limited context. Combining this with evolving threat intelligence reveals whether the IP belongs to a known botnet, including geolocation, which shows the attack origin, and correlating with past incidents indicates if this IP has targeted the organization previously.&lt;/p&gt;
&lt;p&gt;Alert aggregation prevents duplicate notifications for the same incident. When an attacker probes multiple endpoints, each probe might trigger individual alerts. Aggregate these into a single incident showing the attack&apos;s scope rather than flooding the team with hundreds of similar notifications.&lt;/p&gt;
&lt;h2&gt;Managing alert fatigue and team burnout&lt;/h2&gt;
&lt;p&gt;Alert fatigue occurs when you receive so many notifications that they become desensitized, and you miss important security incidents. If your company receives hundreds of alerts a day, you&apos;re likely to get low investigation rates, with analysts ignoring the bulk of alerts.&lt;/p&gt;
&lt;p&gt;Implement alert scoring that combines severity, context, and historical accuracy. Alerts that frequently lead to confirmed security incidents receive higher scores than those with poor signal-to-noise ratios. Machine learning models can predict which alerts warrant investigation based on key components like time of day, user reputation scores, and historical attack patterns. This scoring helps security analysts prioritize workloads when alert volumes exceed capacity.&lt;/p&gt;
&lt;p&gt;Establish alert ownership and escalation paths. Each alert type needs a designated team responsible for investigation and remediation. Application security alerts route to security teams familiar with the codebase, infrastructure alerts go to operations, and access control alerts might escalate to the security team. Clear ownership prevents alerts from languishing in shared queues where everyone assumes someone else will investigate.&lt;/p&gt;
&lt;h2&gt;Setting up alerts with Honeybadger Insights&lt;/h2&gt;
&lt;p&gt;Applications generate a constant stream of events like failed logins, suspicious input, permission changes, and unexpected system or user behavior. Instead of sending raw log files into a traditional SIEM system pipeline and configuring complex agents, Honeybadger Insights is &lt;a href=&quot;https://www.honeybadger.io/tour/logging-observability/&quot;&gt;an observability tool&lt;/a&gt; that gives you structured security events directly from your application. These events become searchable, filterable, and alertable. This allows you to detect and respond to potential threats in real time without heavy infrastructure.&lt;/p&gt;
&lt;p&gt;The idea is simple: treat security threats like application telemetry. Whenever something suspicious happens, you send a structured event to Honeybadger Insights. Then you create queries and alerts that behave like SIEM system rules.&lt;/p&gt;
&lt;p&gt;For this guide, we will work with Nodejs. First, install the Honeybadger JavaScript package in your Node.js application. This example uses a basic Express server that reports failed login attempts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm init -y
npm install express @honeybadger-io/js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a file named &lt;code&gt;server.js&lt;/code&gt; and configure Honeybadger.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const express = require(&amp;quot;express&amp;quot;);
const Honeybadger = require(&amp;quot;@honeybadger-io/js&amp;quot;);
const app = express();

app.use(express.json());

// Initialize Honeybadger
Honeybadger.configure({
  apiKey: process.env.HONEYBADGER_API_KEY,
  environment: &amp;quot;production&amp;quot;,
});

// Simulated login endpoint
app.post(&amp;quot;/login&amp;quot;, (req, res) =&amp;gt; {
  const { username, password } = req.body;

  // Fake authentication logic
  const isValid = username === &amp;quot;admin&amp;quot; &amp;amp;&amp;amp; password === &amp;quot;secret&amp;quot;;

  if (!isValid) {
    // Send a SIEM-style security event to Honeybadger Insights
    Honeybadger.event({
      type: &amp;quot;security.login.failed&amp;quot;,
      user: username,
      ip: req.ip,
      timestamp: new Date().toISOString(),
      metadata: {
        reason: &amp;quot;Invalid credentials&amp;quot;,
      },
    });

    return res.status(401).json({ error: &amp;quot;Invalid credentials&amp;quot; });
  }

  res.json({ message: &amp;quot;Login successful&amp;quot; });
});

// Example: suspicious input detection
app.post(&amp;quot;/search&amp;quot;, (req, res) =&amp;gt; {
  const { query } = req.body;

  if (query &amp;amp;&amp;amp; query.includes(&amp;quot;&apos; OR 1=1&amp;quot;)) {
    Honeybadger.event({
      type: &amp;quot;security.sql_injection.detected&amp;quot;,
      ip: req.ip,
      query,
      timestamp: new Date().toISOString(),
    });
  }

  res.json({ results: [] });
});

app.listen(3000, () =&amp;gt; {
  console.log(&amp;quot;Server running on port 3000&amp;quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this setup, each security action is shown as a structured event. Instead of parsing logs later, Honeybadger stores these events in Insights, where they can be queried like a lightweight SIEM system dataset.&lt;/p&gt;
&lt;p&gt;After events start flowing, you create SIEM-style alerts inside Honeybadger. For example, you can define a query such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;type:&amp;quot;security.login.failed&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and configure an alert when the event count exceeds a threshold within a time window. This allows you to detect brute-force attacks, suspicious traffic spikes, or repeated injection attempts. You can also filter by IP address, user, environment, or any custom alert metadata you send.&lt;/p&gt;
&lt;p&gt;To run the code locally, create a &lt;code&gt;.env&lt;/code&gt; file or export your API key in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export HONEYBADGER_API_KEY=your_api_key_here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then simulate events using curl or Postman:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST http://localhost:3000/login \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &apos;{&amp;quot;username&amp;quot;:&amp;quot;admin&amp;quot;,&amp;quot;password&amp;quot;:&amp;quot;wrong&amp;quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each failed request will appear in Honeybadger Insights within seconds. You can repeat the request multiple times to trigger your alert thresholds.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/siem-alerts/honeybadger-insights.png&quot; alt=&quot;Honeybadger Insights dashboard for SIEM alerts&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This approach turns your application into the primary source of security intelligence. Instead of forwarding system logs and building fragile parsing rules, you show events at the moment the risk occurs. The result is faster threat detection, cleaner security data, and SIEM-style security monitoring without the operational overhead of traditional security pipelines.&lt;/p&gt;
&lt;p&gt;If you want to take this further, you can extend the pattern to permission changes, rate-limit violations, token misuse, or unusual API access patterns.&lt;/p&gt;
&lt;h2&gt;Benefits of Honeybadger for SIEM alerts&lt;/h2&gt;
&lt;p&gt;Traditional enterprise SIEM solutions require significant investment in licensing, infrastructure, and specialized personnel. Other security solutions like Splunk, QRadar, and ArcSight need you to have a dedicated security operations center and security teams trained in complex query languages. These platforms also require deep integration work to connect with your existing infrastructure and other security tools already in your stack.&lt;/p&gt;
&lt;p&gt;That&apos;s not the case for Honeybadger compared to other tools used for security event management. It removes these barriers through developer-focused integration, so you focus more on improving security posture. Setup takes minutes, and installing the package requires just a few commands. The platform automatically captures events through existing error and performance monitoring instrumentation, which lets you enhance threat detection capabilities (improve security posture) without overhauling your entire toolchain.&lt;/p&gt;
&lt;p&gt;Alert configuration happens through intuitive web interfaces. Creating a SIEM solution alert resembles configuring a performance threshold, which involves selecting the event pattern, defining the threshold, and specifying notification channels. Structured logging lets you query and analyze data immediately. Notifications integrate seamlessly through Slack, PagerDuty, and webhooks. The platform combines SIEM system capabilities with error tracking and performance monitoring in a single interface to provide a unified dashboard for security incident investigation.&lt;/p&gt;
&lt;p&gt;You can sign up for a &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;free trial of Honeybadger&lt;/a&gt; to see if this will fit your company&#x2019;s needs.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Test-Commit-Revert: A useful workflow for testing legacy code in Ruby</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/ruby-tcr-test-commit-revert/"/>
    <id>https://www.honeybadger.io/blog/ruby-tcr-test-commit-revert/</id>
    <published>2020-10-06T00:00:00+00:00</published>
    <updated>2026-05-14T00:00:00+00:00</updated>
    <author>
      <name>Jos&#xe9; M. Gilgado</name>
    </author>
    <summary type="text">When you inherit a legacy app with no tests, your first step should be to add them. But that can be a huge task! How do you even start? In this article, Jos&#xe9; will introduce us to a testing workflow called test-commit-revert (TCR) that is particularly useful for adding tests to legacy systems. Read to see practical examples and how to set up your tooling for minimal friction.</summary>
    <content type="html">&lt;p&gt;It happens to all of us. As software projects grow, parts of the production code we ship end up without a comprehensive test suite. When you take another look at the same area of code after a few months, it may be difficult to understand; even worse, there might be a bug, and we don&apos;t know where to begin fixing it.&lt;/p&gt;
&lt;p&gt;Modifying production code without tests is a major challenge. We can&apos;t be sure if we&apos;ll break anything in the process, and checking everything manually is, at best, prone to mistakes; usually, it&apos;s impossible.&lt;/p&gt;
&lt;p&gt;Dealing with this kind of code is one of the most common tasks we perform as developers, and many techniques have focused on this issue over the years, such as &lt;a href=&quot;https://www.honeybadger.io/blog/ruby-legacy-characterization-test/&quot;&gt;characterization tests&lt;/a&gt;, which we discussed in a previous article.&lt;/p&gt;
&lt;p&gt;Today, we&apos;ll cover another technique based on characterization tests (test-commit-revert) and introduced by Kent Beck, who also introduced TDD to the modern programming world many years ago.&lt;/p&gt;
&lt;h2&gt;What&apos;s TCR?&lt;/h2&gt;
&lt;p&gt;TCR stands for &amp;quot;test, commit, revert&amp;quot;, but it&apos;s more accurate to call it &amp;quot;test &amp;amp;&amp;amp; commit || revert&amp;quot;. Let&apos;s see why.&lt;/p&gt;
&lt;p&gt;This technique describes a workflow to test legacy code. We&apos;ll use a script that will run the tests every time we save our project files. The process is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, we create an empty unit test for the part of the legacy code we want to test.&lt;/li&gt;
&lt;li&gt;We then add a single assertation and save the test.&lt;/li&gt;
&lt;li&gt;Since we have our script set up, the test is automatically run. If it succeeds, the change is committed. If the test fails, the change is deleted (reverted), and we need to try again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the test passes, we can then add a new test case.&lt;/p&gt;
&lt;p&gt;Essentially, TCR (test, commit, revert) is about keeping your code in a &amp;quot;green&amp;quot; state instead of writing a failing test first (red) and then make it pass (green), as we do with test-driven development. If we write a failing test, it&apos;ll just vanish, and we&apos;ll be brought back to the &amp;quot;green&amp;quot; state again.&lt;/p&gt;
&lt;h2&gt;Purpose of test-commit-revert&lt;/h2&gt;
&lt;p&gt;The main goal of this technique is to understand the code a bit better each time you add a test case. This will naturally increase the test coverage and unblock many refactorings that, otherwise, wouldn&apos;t be possible.&lt;/p&gt;
&lt;p&gt;One of the advantages of test, commit, revert is that it&apos;s useful in many scenarios. We can use it with production code that has no tests at all or with code that&apos;s partially tested. If the tests fail, we just revert the change and try again.&lt;/p&gt;
&lt;h2&gt;How can we use it?&lt;/h2&gt;
&lt;p&gt;Kent Beck shows, in different articles and videos (linked at the end), that a good approach is using a script that runs after certain files in the project are saved.&lt;/p&gt;
&lt;p&gt;This will depend heavily on the project you&apos;re trying to test. Something like the following script, which is executed every time we save files with a plugin in the editor, is a good start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(rspec &amp;amp;&amp;amp; git commit -am &amp;quot;WIP&amp;quot;) || git reset --hard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re using Visual Studio Code, a good plugin to execute on every save is &lt;a href=&quot;https://github.com/emeraldwalk/vscode-runonsave&quot;&gt;&amp;quot;runonsave&amp;quot;&lt;/a&gt;. You can include the above command or a similar one for your project. In this case, the whole config file would be&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;folders&amp;quot;: [{ &amp;quot;path&amp;quot;: &amp;quot;.&amp;quot; }],
  &amp;quot;settings&amp;quot;: {
    &amp;quot;emeraldwalk.runonsave&amp;quot;: {
      &amp;quot;commands&amp;quot;: [
        {
          &amp;quot;match&amp;quot;: &amp;quot;*.rb&amp;quot;,
          &amp;quot;cmd&amp;quot;: &amp;quot;cd ${workspaceRoot} &amp;amp;&amp;amp; rspec &amp;amp;&amp;amp; git commit -am WIP || git reset --hard&amp;quot;
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember that later, you can squash the commit with Git directly in the command line or when merging the PR if you&apos;re using Github:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/ruby-tcr-test-commit-revert/squash-github.png&quot; alt=&quot;Squash commits on Github&quot; title=&quot;Squash commits on Github&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This means we&apos;ll only get one commit in the main branch for all the commits we did on the branch we&apos;re working on. This diagram from Github explains it well:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/ruby-tcr-test-commit-revert/commit-squashing-diagram.png&quot; alt=&quot;Diagram squashed commits on Github&quot; title=&quot;test commit revert: Diagram squashed commits on Github&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Writing our first test with TCR&lt;/h2&gt;
&lt;p&gt;We&apos;ll use a simple example to illustrate the technique. We have a class that we know is working, but we need to modify it.&lt;/p&gt;
&lt;p&gt;We could just make a change and deploy the changes to production. However, we want to be sure that we don&apos;t break anything in the process, which is always a good idea.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# worker.rb
class Worker
  def initialize(age, active_years, veteran)
    @age = age
    @active_years = active_years
    @veteran = veteran
  end

  def can_retire?
    return true if @age &amp;gt;= 67
    return true if @active_years &amp;gt;= 30
    return true if @age &amp;gt;= 60 &amp;amp;&amp;amp; @active_years &amp;gt;= 25
    return true if @veteran &amp;amp;&amp;amp; @active_years &amp;gt; 25

    false
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first step would be to create a new file for the tests, so we can start adding them there. We&apos;ve seen the first line in the &lt;code&gt;can_retire?&lt;/code&gt; method with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;  def can_retire?
    return true if @age &amp;gt;= 67
    ...
    ...
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus, we can test this case first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# specs/worker_spec.rb
require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &amp;quot;should return true if age is higher than 67&amp;quot; do

    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s a quick tip: when you&apos;re working with test, commit, revert, every time you save, the latest changes will disappear if the tests fail. Therefore, we want to have as much code as possible to &amp;quot;set up&amp;quot; the test before actually writing and saving the line or lines with the assertion.&lt;/p&gt;
&lt;p&gt;If we save the above file like that, we can then add a line for the test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &amp;quot;should return true if age is higher than 67&amp;quot; do
      expect(Worker.new(70, 10, false).can_retire?).to be_true ## This line can disappear when we save now
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we save, if the new line doesn&apos;t vanish, we&apos;ve done a good job; the test passes!&lt;/p&gt;
&lt;h2&gt;Adding more tests&lt;/h2&gt;
&lt;p&gt;Once we have our first test, we can keep adding more cases while taking into account false cases. After some work, we have something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# frozen_string_literal: true

require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &apos;should return true if age is higher than 67&apos; do
      expect(Worker.new(70, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is 67&apos; do
      expect(Worker.new(67, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is less than 67&apos; do
      expect(Worker.new(50, 10, false).can_retire?).to be false
    end

    it &apos;should return true if active years is higher than 30&apos; do
      expect(Worker.new(60, 31, false).can_retire?).to be true
    end

    it &apos;should return true if active years is 30&apos; do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In every case, we write the &amp;quot;it&amp;quot; block first, save, and then add the assertion with &lt;code&gt;expect(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As usual, we can add as many tests as possible, but it makes sense to avoid adding too many once we&apos;re relatively sure that everything is covered.&lt;/p&gt;
&lt;p&gt;There are still a few cases to cover, so we should add them just for completeness.&lt;/p&gt;
&lt;h2&gt;Final tests&lt;/h2&gt;
&lt;p&gt;Here&apos;s the spec file in its final form. As you can see, we could still add more cases, but I think this is enough to illustrate the process of TCR.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# frozen_string_literal: true

require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &apos;should return true if age is higher than 67&apos; do
      expect(Worker.new(70, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is 67&apos; do
      expect(Worker.new(67, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is less than 67&apos; do
      expect(Worker.new(50, 10, false).can_retire?).to be false
    end

    it &apos;should return true if active years is higher than 30&apos; do
      expect(Worker.new(60, 31, false).can_retire?).to be true
    end

    it &apos;should return true if active years is 30&apos; do
      expect(Worker.new(20, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is higher than 60 and active years is higher than 25&apos; do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is higher than 60 and active years is higher than 25&apos; do
      expect(Worker.new(61, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is 60 and active years is higher than 25&apos; do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is higher than 60 and active years is 25&apos; do
      expect(Worker.new(61, 25, false).can_retire?).to be true
    end

    it &apos;should return true if age is 60 and active years is 25&apos; do
      expect(Worker.new(60, 25, false).can_retire?).to be true
    end

    it &apos;should return true if is veteran and active years is higher than 25&apos; do
      expect(Worker.new(60, 25, false).can_retire?).to be true
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ways to refactor&lt;/h2&gt;
&lt;p&gt;If you&apos;ve read this far, there&apos;s probably something that feels a bit off with the code. We have many &amp;quot;magical numbers&amp;quot; that should be extracted into constants, both in the test and in the Worker class.&lt;/p&gt;
&lt;p&gt;We could also create private methods for each case in the main can_retire? public method.&lt;/p&gt;
&lt;p&gt;I&apos;ll leave both potential refactorings as exercises for you. However, we have tests now, so if we make a mistake in any step, they will tell us.&lt;/p&gt;
&lt;h2&gt;Where do you go from here?&lt;/h2&gt;
&lt;p&gt;I encourage you to try test-commit-revert with your projects and production code. It&apos;s a very cheap experiment because you don&apos;t need any fancy continuous integration in an external server or a dependency with a new library. All you need is a way to execute a command every time you save certain files on your computer.&lt;/p&gt;
&lt;p&gt;It&apos;ll also give you a &amp;quot;gaming&amp;quot; experience when adding tests, which is always fun and interesting. Additionally, the discipline of having failing tests removed from your editor (a safety measure that kicks in whenever tests fail) will give you an extra safety net by confirming that the tests you&apos;re pushing to the repository are actually passing.&lt;/p&gt;
&lt;p&gt;I hope you find this new technique useful when dealing with legacy code. I&apos;ve used multiple times in the last few months, and it&apos;s always been a pleasure.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Tips for upgrading Python/Django versions in existing apps</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/tips-for-upgrading-python-django-versions-in-existing-apps/"/>
    <id>https://www.honeybadger.io/blog/tips-for-upgrading-python-django-versions-in-existing-apps/</id>
    <published>2023-07-20T00:00:00+00:00</published>
    <updated>2026-05-05T00:00:00+00:00</updated>
    <author>
      <name>Michael Barasa</name>
    </author>
    <summary type="text">Unlock the power of the latest Python and Django versions with expert tips for seamlessly upgrading Python apps.</summary>
    <content type="html">&lt;p&gt;Python is a robust and powerful programming language. In addition to machine learning, Python can be used for tasks such as web scraping, image processing, scientific computing, and much more. A framework such as Django, which is built on top of Python, enables you to build beautiful web applications&#x2014;top websites such as Dropbox, Instagram, and YouTube use Django.&lt;/p&gt;
&lt;p&gt;However, as you create applications and release them to consumers, upgrading to the latest Python version becomes essential to keep pace with updates and new features. For instance, Python 3.11 was released in October 2022, sporting various bug fixes. Before that, there was &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.10, 3.9, 3.8&lt;/a&gt;, and others. There are also numerous third-party libraries that you should regularly watch out for.&lt;/p&gt;
&lt;p&gt;Incorporating these updates into our applications can be beneficial, as doing so can enhance the security and reliability. However, doing this incorrectly could also break your application.&lt;/p&gt;
&lt;h2&gt;Why you should be upgrading your Python version&lt;/h2&gt;
&lt;p&gt;Upgrading your Python and Django applications to the latest Python version is important for several reasons.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;New Python versions come with additional features and improvements. Newer Django versions can help reduce boilerplate code, enabling you to develop and push products to the market much faster. They can also help you learn new ways of adding functionalities to your application that can be more fun and easier to implement.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;New Python versions can help with bug fixes in your code. Apart from enabling your application to behave as expected, eliminating bugs also streamlines the software development process, meaning that you&apos;ll be less frustrated. A Python upgrade can improve your app&apos;s stability during production.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upgrading your application also makes it easier to maintain your codebase. You can quickly incorporate changes in your project without major downtimes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensuring you&apos;re using the latest Python version and Django version also improves security. Hackers are always looking for new ways to infiltrate systems, steal data, install malicious files and viruses, and just wreak havoc. Upgrading your application to use the latest Python version or Django ensures that you have the latest security updates.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now that we understand why upgrading applications is necessary, let&apos;s jump into the action itself.&lt;/p&gt;
&lt;h2&gt;Things to know before updating&lt;/h2&gt;
&lt;p&gt;Before updating the Python version, you should read the changes in the newest Python release. Go through the official documentation, particularly the release notes. Note that with each new version, Django releases accompanying documentation, which is quite helpful when upgrading applications.&lt;/p&gt;
&lt;p&gt;Next, look at the functions, objects, and other items deprecated in the latest Python version. Consider the deadline or timeline in which these deprecation changes are set to come into effect. Understanding the deprecation timeline allows you to better plan your upgrade process, including the prioritization of certain tasks over others.&lt;/p&gt;
&lt;p&gt;You should also keep an eye on backwards compatibility. Remember that some things that work in your existing application may fail when you upgrade to a new Python version. This is why it&apos;s recommended to test things first before pushing to production. Whenever an upgrade causes your app to break, you can roll back to the previous Python version as you figure out the next steps.&lt;/p&gt;
&lt;h2&gt;How to update Python/Django apps&lt;/h2&gt;
&lt;p&gt;In this section, we will learn how to upgrade Python and Django in a simple Django Movie API to use a newer Python version. You can download the project&apos;s code from this &lt;a href=&quot;https://github.com/WanjaMIKE/simpledjangobookapi/&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Check Django and other library versions&lt;/h3&gt;
&lt;p&gt;Once you&apos;ve downloaded the Movie API project and finished setting it up on your computer, the first step is to check the versions of the dependencies that the project uses.&lt;/p&gt;
&lt;p&gt;To do this, launch the project in your preferred code editor and open the &lt;code&gt;requirements.txt&lt;/code&gt; file. All dependencies used by the project are listed in this file, as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;asgiref==3.3.1
Django==3.1.8
django-filter==2.4.0
djangorestframework==3.12.2
djangorestframework-simplejwt==4.6.0
pytz==2021.1
sqlparse==0.4.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we now know the versions in use (shown above), the next step is to determine the current official version from the documentation. For instance, the latest official Django version is 4.2.1; our application uses an older version (3.1.8), so there&apos;s a need to upgrade.&lt;/p&gt;
&lt;h3&gt;Activate local debugging&lt;/h3&gt;
&lt;p&gt;Trying to update things in production could cause them to break and lead to poor user experience and other negative consequences. You should test your applications locally and ensure that everything is working before pushing them to production.&lt;/p&gt;
&lt;p&gt;You can activate local debugging by setting the &lt;code&gt;debug&lt;/code&gt; option in the &lt;code&gt;settings.py&lt;/code&gt; file to &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# settings.py
import os

# Build paths inside the project like this: os.path.join(BASE_DIR, &#x2026;)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/wa

# SECURITY WARNING: Don&#x2019;t run with debug turned on in production!
DEBUG = True  # Set debug to true

ALLOWED_HOSTS = []
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Run Django checks with python -Wall manage.py check&lt;/h3&gt;
&lt;p&gt;Before starting the upgrade, we&apos;ll need to run several checks to determine if there are any deprecation warnings in our project.&lt;/p&gt;
&lt;p&gt;Deprecation warnings show that certain features will stop working at some point simply because they have been replaced by better alternatives.&lt;/p&gt;
&lt;p&gt;You can check for any warnings using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;python -Wa manage.py test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install new dependencies&lt;/h3&gt;
&lt;p&gt;Now that we have enabled local debugging and identified any deprecation warnings in our project, it&apos;s time to install new dependencies in our project.&lt;/p&gt;
&lt;p&gt;To get the latest Django version, we use the following command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -U Django
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run the above command, the following output should appear in your terminal. We have now successfully updated Django to ver 4.2.1, asgiref to ver 3.6.0, and tzdata to ver 2023.3.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Requirement already satisfied: Django in c:\users\wanjamike\appdata\local\programs\python\python310\lib\site-packages (3.1.8)
Collecting Django
  Downloading Django-4.2.1-py3-none-any.whl (8.0 MB)
     &#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501; 8.0/8.0 MB 43.5 kB/s eta 0:00:00
Requirement already satisfied: sqlparse&amp;gt;=0.3.1 in c:\users\wanjamike\appdata\local\programs\python\python310\lib\site-packages (from Django) (0.4.1)
Collecting tzdata
  Using cached tzdata-2023.3-py2.py3-none-any.whl (341 kB)
Collecting asgiref&amp;lt;4,&amp;gt;=3.6.0
  Using cached asgiref-3.6.0-py3-none-any.whl (23 kB)
Installing collected packages: tzdata, asgiref, Django
  Attempting uninstall: asgiref
    Found existing installation: asgiref 3.3.1
    Uninstalling asgiref-3.3.1:
      Successfully uninstalled asgiref-3.3.1
  Attempting to uninstall: Django
    Found existing installation: Django 3.1.8
    Uninstalling Django-3.1.8:
      Successfully uninstalled Django-3.1.8
Successfully installed Django-4.2.1 asgiref-3.6.0 tzdata-2023.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can update the remaining dependencies in our project using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -U django-filter djangorestframework djangorestframework-simplejwt pytz sqlparse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Successfully installed django-filter-23.2 djangorestframework-3.14.0 djangorestframework-simplejwt-5.2.2 pytz-2023.3 sqlparse-0.4.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s generate a new &lt;code&gt;requirements.txt&lt;/code&gt; file and double-check that we have the latest libraries in our project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you open the &lt;code&gt;requirements.txt&lt;/code&gt;, you should find all the latest dependencies, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;asgiref==3.6.0
Django==4.2.1
django-filter==23.2
djangorestframework==3.14.0
djangorestframework-simplejwt==5.2.2
pytz==2023.3
sqlparse==0.4.4
tzdata==2023.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Check methods and functions&lt;/h3&gt;
&lt;p&gt;In most software updates, developers will try to change how certain functions or components work to improve performance, efficiency, &lt;a href=&quot;https://www.honeybadger.io/blog/reducing-your-python-apps-memory-footprint/&quot;&gt;memory usage&lt;/a&gt;, or convenience. Rather than fighting such changes, consider embracing and integrating them into your application to realize their benefits.&lt;/p&gt;
&lt;p&gt;Note that while this may be a time-consuming step, the benefits are worth it. You should go through the official documentation to understand new changes and learn how you can integrate them into your application.&lt;/p&gt;
&lt;p&gt;Let&apos;s review some of the changes in Django 4.2 that can help you in future upgrades.&lt;/p&gt;
&lt;p&gt;First, support for MariaDB 10.3, MySQL 5.7, and PostgreSQL 11 databases was dropped. If your application was using any of these database versions, consider upgrading. Make sure to back up your data before upgrading to avoid service disruptions.&lt;/p&gt;
&lt;p&gt;Second, Django 4.2 changed how we do data indexing. In the past, we indexed data using the statement below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;index_together = [[&amp;quot;rank&amp;quot;, &amp;quot;name&amp;quot;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But now we must call the &lt;code&gt;Index&lt;/code&gt; method from the &lt;code&gt;models&lt;/code&gt; class, as demonstrated below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;indexes = [models.Index(fields=[&amp;quot;rank&amp;quot;, &amp;quot;name&amp;quot;])]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Third, the &lt;code&gt;length_is&lt;/code&gt; template filter is now deprecated and has instead been replaced by the traditional &lt;code&gt;==&lt;/code&gt; operator.&lt;/p&gt;
&lt;p&gt;Don&apos;t do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;{% if value|length_is:4 %}&#x2026;{% endif %}
{{ value|length_is:4 }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;{% if value|length == 4 %}&#x2026;{% endif %}
{% if value|length == 4 %}True{% else %}False{% endif %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Updating Python is important&lt;/h2&gt;
&lt;p&gt;Updates are a normal thing during software development. Therefore, it&apos;s best to learn how to update your app&apos;s Python version and integrate changes in our applications. Apart from improved security, software updates allow us to deal with bugs and ensure that our code is maintainable.&lt;/p&gt;
&lt;p&gt;When upgrading to a new version of Python, make sure to test things out in your local environment before pushing them to production. We don&apos;t want things to break and cause a poor user experience.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Errors in Python: types, causes, and examples</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/errors-in-python/"/>
    <id>https://www.honeybadger.io/blog/errors-in-python/</id>
    <published>2026-04-27T00:00:00+00:00</published>
    <updated>2026-04-27T00:00:00+00:00</updated>
    <author>
      <name>Aditya Raj</name>
    </author>
    <summary type="text">Errors in Python can arise from invalid syntax, unexpected issues during execution, system issues, or flaws in program logic. Learn about the different types of Python errors and practical ways to identify and avoid them to build more reliable programs.</summary>
    <content type="html">&lt;p&gt;Errors in Python are issues in a program that cause incorrect results or prevent proper execution. Some Python errors are loud and obvious, and your code barely gets started before it throws an error that tells you exactly what went wrong. Other errors are more subtle, allowing your Python program to run without complaints while silently producing incorrect results that only become apparent later. These differences become clearer when you group errors in Python based on how they occur and how they impact execution.&lt;/p&gt;
&lt;p&gt;For example, syntax errors get caught before the code even runs, and runtime errors blow up mid-execution. System-level errors have nothing to do with your code&#x2014;and they still stop execution&#x2014;while logical errors don&#x2019;t stop execution but produce incorrect results. Understanding the different types of errors and learning how to avoid them is essential for writing reliable and robust code. Let&apos;s look at the different types of errors in Python, their causes, and how to avoid them.&lt;/p&gt;
&lt;h2&gt;Different types of errors in Python&lt;/h2&gt;
&lt;p&gt;We can broadly categorize Python errors into four types, as shown in the following image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/errors-in-python/errors-in-python.png&quot; alt=&quot;Diagram showing different types of errors in Python&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Syntax errors&lt;/strong&gt;: Syntax errors in Python occur due to invalid syntax, incorrect indentation, or typos. These errors are detected before the program is executed, while the Python interpreter parses the code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime errors&lt;/strong&gt;: In Python, runtime errors occur during execution when the program encounters an invalid operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System-level errors&lt;/strong&gt;: System-level errors in Python are raised by the runtime environment or the operating system due to issues such as memory overflow or interruptions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logical errors&lt;/strong&gt;: Logical errors occur when the program&apos;s logic is incorrect, even though the code runs without errors, producing incorrect results.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&apos;s discuss all these Python error types one by one in detail with examples, starting with syntax errors.&lt;/p&gt;
&lt;h2&gt;Syntax errors in Python&lt;/h2&gt;
&lt;p&gt;Syntax errors are the errors caused by invalid code structure, typos, incorrect indentation, etc. For example, an &lt;code&gt;if&lt;/code&gt; block is defined in Python using the &lt;code&gt;if&lt;/code&gt; keyword, a boolean condition, and the &lt;code&gt;:&lt;/code&gt; character. If we skip &lt;code&gt;:&lt;/code&gt; while writing an &lt;code&gt;if&lt;/code&gt; block in Python, the code runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the error message &lt;code&gt;SyntaxError: expected &apos;:&apos;&lt;/code&gt;, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if x &amp;gt; 10
    print(&amp;quot;Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code gives the following error message:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    if x &amp;gt; 10
             ^
SyntaxError: expected &apos;:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Syntax errors occur when the interpreter cannot parse the code because it violates Python&#x2019;s grammar rules. Let&apos;s discuss some of the common syntax errors in Python.&lt;/p&gt;
&lt;h3&gt;Indentation errors in Python&lt;/h3&gt;
&lt;p&gt;Python uses spaces and tabs for code indentation. The code will run into &lt;code&gt;IndentationError&lt;/code&gt; if a code block doesn&apos;t have correct indentation. For example, defining an &lt;code&gt;if&lt;/code&gt; block requires us to indent the lines in the &lt;code&gt;if&lt;/code&gt; block to the right by two/four spaces. If we don&apos;t indent the lines in the &lt;code&gt;if&lt;/code&gt; block, the code runs into &lt;code&gt;IndentationError&lt;/code&gt; with the error message &lt;code&gt;IndentationError: expected an indented block after &apos;if&apos; statement on line x&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if x &amp;gt; 10:
print(&amp;quot;Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The unindented if block gives the following error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 2
    print(&amp;quot;Honeybadger&amp;quot;)
    ^
IndentationError: expected an indented block after &apos;if&apos; statement on line 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, we need to indent the code block inside a function definition. Not doing so gives us an &lt;code&gt;IndentationError&lt;/code&gt; with the message &lt;code&gt;IndentationError: expected an indented block after function definition on line x&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the print statement inside the &lt;code&gt;say_hello()&lt;/code&gt; function isn&apos;t indented to the right. Hence, the code gives the following error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 2
    print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
    ^
IndentationError: expected an indented block after function definition on line 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you indent a code block, it is important to keep the indentation the same for each statement in the code block. Otherwise, the program runs into &lt;code&gt;IndentationError&lt;/code&gt;. For example, if you indent the first statement of a code block by four spaces and the second statement by two spaces, the program will run into &lt;code&gt;IndentationError&lt;/code&gt; with the error message &lt;code&gt;IndentationError: unindent does not match any outer indentation level&lt;/code&gt;, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
    print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
  print(&amp;quot;Great seeing you here.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    print(&amp;quot;Great seeing you here.&amp;quot;)
                                   ^
IndentationError: unindent does not match any outer indentation level
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if you indent the first statement in a code block by two spaces and the second statement by four spaces, the program will run into &lt;code&gt;IndentationError&lt;/code&gt; with the message &lt;code&gt;IndentationError: unexpected indent&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
  print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
    print(&amp;quot;Great seeing you here.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    print(&amp;quot;Great seeing you here.&amp;quot;)
IndentationError: unexpected indent
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tab error&lt;/h3&gt;
&lt;p&gt;TabError is a specific indentation error caused by mixing tabs and spaces to indent code blocks. For instance, you can indent a statement by the same distance using four spaces or one tab. Visually, it looks the same. However, if you indent a statement in a code block using a tab and another using four spaces, the program will run into &lt;code&gt;TabError&lt;/code&gt; with the error message &lt;code&gt;TabError: inconsistent use of tabs and spaces in indentation&lt;/code&gt;, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
    print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;) # Indentation using Tab
    print(&amp;quot;Great seeing you here.&amp;quot;)  # Indentation using four spaces
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    print(&amp;quot;Great seeing you here.&amp;quot;)  # Indentation using four spaces
TabError: inconsistent use of tabs and spaces in indentation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 3 explicitly disallows mixing tabs and spaces for indentation in a way that makes the meaning ambiguous, and you should always avoid it.&lt;/p&gt;
&lt;h3&gt;Unclosed strings/ brackets/ parentheses&lt;/h3&gt;
&lt;p&gt;A Python program runs into a &lt;code&gt;SyntaxError&lt;/code&gt; if you don&apos;t close a string, parentheses, or a bracket. For example, if you don&apos;t close a string, the program runs into  &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: unterminated string literal&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if x &amp;gt; 10:
    print(&amp;quot;Honeybadger)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the closing &lt;code&gt;&amp;quot;&lt;/code&gt; is missing in the &lt;code&gt;&amp;quot;Honeybadger&lt;/code&gt; string. Due to this, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 2
    print(&amp;quot;Honeybadger)
          ^
SyntaxError: unterminated string literal (detected at line 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if we forget the closing parenthesis while calling a function or defining a tuple, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: &apos;(&apos; was never closed&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;print(&amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    print(&amp;quot;Honeybadger&amp;quot;
         ^
SyntaxError: &apos;(&apos; was never closed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just like parentheses, if we miss the closing bracket while defining a list, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: &apos;[&apos; was never closed&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_list = [1, 2, 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    my_list = [1, 2, 3
              ^
SyntaxError: &apos;[&apos; was never closed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Invalid assignment errors&lt;/h3&gt;
&lt;p&gt;Invalid assignment errors are mostly caused by assigning values to literals or function calls. For example, if we assign the value &lt;code&gt;&amp;quot;Honeybadger&amp;quot;&lt;/code&gt; to a string literal &lt;code&gt;&amp;quot;name&amp;quot;&lt;/code&gt;, the program throws a &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: cannot assign to literal here. Maybe you meant &apos;==&apos; instead of &apos;=&apos;?&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;&amp;quot;name&amp;quot; = &amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    &amp;quot;name&amp;quot; = &amp;quot;Honeybadger&amp;quot;
    ^^^^^^
SyntaxError: cannot assign to literal here. Maybe you meant &apos;==&apos; instead of &apos;=&apos;?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if we assign a value to a Python keyword, the program runs into a &lt;code&gt;SyntaxError&lt;/code&gt;. For example, assigning the value &lt;code&gt;Honeybadger&lt;/code&gt; to a variable &lt;code&gt;class&lt;/code&gt; results in a &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: invalid syntax&lt;/code&gt;, as &lt;code&gt;class&lt;/code&gt; is a Python keyword.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;class = &amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    class = &amp;quot;Honeybadger&amp;quot;
          ^
SyntaxError: invalid syntax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An assignment error also occurs when we miss a &lt;code&gt;=&lt;/code&gt; character while comparing values using the equality operator. For example, if we use &lt;code&gt;=&lt;/code&gt; instead of &lt;code&gt;==&lt;/code&gt; to compare two values, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the error message &lt;code&gt;SyntaxError: invalid syntax. Maybe you meant &apos;==&apos; or &apos;:=&apos; instead of &apos;=&apos;?&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;name = &amp;quot;Honeybadger&amp;quot;
input_string = &amp;quot;Honeybadger&amp;quot;
if name = input_string:
  print(name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    if name = input_string:
       ^^^^^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant &apos;==&apos; or &apos;:=&apos; instead of &apos;=&apos;?
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to avoid syntax errors in Python?&lt;/h3&gt;
&lt;p&gt;Syntax errors occur due to incorrect indentation, mismatched delimiters, missing punctuation, invalid variable names, or incorrect operators. You can avoid syntax errors using the following best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always add the required colon &lt;code&gt;:&lt;/code&gt; after block statements like &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;def&lt;/code&gt;, and &lt;code&gt;class&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Avoid using reserved keywords such as &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, or &lt;code&gt;return&lt;/code&gt; as variable names.&lt;/li&gt;
&lt;li&gt;Always close all the parentheses &lt;code&gt;()&lt;/code&gt;, brackets &lt;code&gt;[]&lt;/code&gt;, and braces &lt;code&gt;{}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Close all string literals with matching quotation marks &lt;code&gt;&apos;&lt;/code&gt; or &lt;code&gt;&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Follow Python syntax rules and ensure statements are written in the correct format.&lt;/li&gt;
&lt;li&gt;Maintain consistent indentation, preferably using 4 spaces per indentation level, and do not mix tabs and spaces for indentation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition to the above practices, you can use IDEs or code editors such as PyCharm, Spyder, or VS Code that provide syntax highlighting and linting to detect syntax errors early.&lt;/p&gt;
&lt;h2&gt;Runtime errors in Python&lt;/h2&gt;
&lt;p&gt;Runtime errors occur in a Python program after it passes the syntax check and starts executing, when something goes wrong. Examples of runtime errors in Python include &lt;code&gt;ZeroDivisionError&lt;/code&gt;, &lt;code&gt;NameError&lt;/code&gt;, &lt;code&gt;TypeError&lt;/code&gt;, and &lt;code&gt;ValueError&lt;/code&gt;. Let&#x2019;s discuss the different runtime errors, their causes, and ways to avoid them.&lt;/p&gt;
&lt;h3&gt;Zero division error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ZeroDivisionError&lt;/code&gt; exception is one of the most common arithmetic errors that occurs if the denominator of a division operation is zero.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;x = 10 / 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we are dividing 10 by 0. Hence, the program runs into &lt;code&gt;ZeroDivisionError&lt;/code&gt; with the error message &lt;code&gt;ZeroDivisionError: division by zero&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    x = 10 / 0
ZeroDivisionError: division by zero
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Name error&lt;/h3&gt;
&lt;p&gt;The NameError exception is a runtime error that occurs when a variable is referenced before assignment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;y = x / 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we tried to divide &lt;code&gt;x&lt;/code&gt; by 10 without defining &lt;code&gt;x&lt;/code&gt; or assigning it a value. Hence, the variable name &lt;code&gt;x&lt;/code&gt;  isn&apos;t present in the scope of the program, and the program runs into a &lt;code&gt;NameError&lt;/code&gt; exception with the error message &lt;code&gt;NameError: name &apos;x&apos; is not defined&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    y = x / 10
NameError: name &apos;x&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;NameError&lt;/code&gt; exception also occurs if you use a variable first and define it later in the program. For example, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;print(greeting)
greeting = &amp;quot;Hi, you are at Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we have referenced the variable &lt;code&gt;greeting&lt;/code&gt; and later assigned it a value. Hence, the program throws a &lt;code&gt;NameError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    print(greeting)
NameError: name &apos;greeting&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Unbound local error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;UnboundLocalError&lt;/code&gt; exception occurs when a local variable is referenced before assignment, in a function or method. For instance, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello():
    print(greeting)
    greeting = &amp;quot;Hi, you are at Honeybadger&amp;quot;
    
say_hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we have referenced the variable &lt;code&gt;greeting&lt;/code&gt; before assigning it a value in the &lt;code&gt;say_hello()&lt;/code&gt; function. When we call the &lt;code&gt;say_hello()&lt;/code&gt; function, the program raises the &lt;code&gt;UnboundLocalError&lt;/code&gt; exception with the message &lt;code&gt;UnboundLocalError: local variable &apos;greeting&apos; referenced before assignment&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 5, in &amp;lt;module&amp;gt;
    say_hello()
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in say_hello
    print(greeting)
UnboundLocalError: local variable &apos;greeting&apos; referenced before assignment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, if we hadn&apos;t assigned any value to the variable after the print statement, the program would have run into a &lt;code&gt;NameError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello():
    print(greeting)
    print(&amp;quot;Hi, you are at Honeybadger&amp;quot;)

say_hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 5, in &amp;lt;module&amp;gt;
    say_hello()
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in say_hello
    print(greeting)
NameError: name &apos;greeting&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Type error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;TypeError&lt;/code&gt; exceptions in Python occur when an operation is applied to a value or variable of an incompatible data type. For example, adding an integer and a string results in a &lt;code&gt;TypeError&lt;/code&gt; exception with the error message &lt;code&gt;TypeError: unsupported operand type(s) for +: &apos;int&apos; and &apos;str&apos;&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;x = 10 + &amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    x = 10 + &amp;quot;Honeybadger&amp;quot;
TypeError: unsupported operand type(s) for +: &apos;int&apos; and &apos;str&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, calling a non-callable object or iterating on a non-iterable object also results in a &lt;code&gt;TypeError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;name = &amp;quot;Honeybadger&amp;quot;
name()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    name()
TypeError: &apos;str&apos; object is not callable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we defined a string variable &lt;code&gt;name&lt;/code&gt; and tried to use it as a function in the second line. Hence, the program throws a &lt;code&gt;TypeError&lt;/code&gt; exception with the message &lt;code&gt;TypeError: &apos;str&apos; object is not callable&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Value error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ValueError&lt;/code&gt; exception occurs in a Python program when we use an input or a variable with the correct data type but an inappropriate value. For instance, the &lt;code&gt;int()&lt;/code&gt; function converts a string to an integer. If the string passed to the &lt;code&gt;int()&lt;/code&gt; function cannot be converted into an integer, the program runs into a &lt;code&gt;ValueError&lt;/code&gt; exception, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;x = int(&amp;quot;10&amp;quot;)
y = x + int(&amp;quot;Honeybadger&amp;quot;)
print(y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the statement &lt;code&gt;x=int(&amp;quot;10&amp;quot;)&lt;/code&gt; executes successfully because &lt;code&gt;&amp;quot;10&amp;quot;&lt;/code&gt; is successfully converted into an integer. However, the string &lt;code&gt;&amp;quot;Honeybadger&amp;quot;&lt;/code&gt; cannot be converted to an integer. Hence, the second line of the code raises a &lt;code&gt;ValueError&lt;/code&gt; exception with the error message &lt;code&gt;ValueError: invalid literal for int() with base 10: &apos;Honeybadger&apos;&lt;/code&gt;, as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    y = x + int(&amp;quot;Honeybadger&amp;quot;)
ValueError: invalid literal for int() with base 10: &apos;Honeybadger&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, square roots aren&apos;t defined for negative numbers. Hence, passing a negative number to the &lt;code&gt;math.sqrt()&lt;/code&gt; function results in a &lt;code&gt;ValueError&lt;/code&gt; exception due to an inappropriate value.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import math
x = math.sqrt(-10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    x = math.sqrt(-10)
ValueError: math domain error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, -10 has the correct integer data type that the &lt;code&gt;sqrt()&lt;/code&gt; function requires. However, it is an invalid value, and we get a &lt;code&gt;ValueError&lt;/code&gt; exception with the message &lt;code&gt;ValueError: math domain error&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Index error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;IndexError&lt;/code&gt; exception occurs in a Python program when we try to access an element at an index that doesn&apos;t exist in an iterable object like a string, list, or tuple. For example, if a list has six elements and we try to access the element at index 6 (the seventh element), the program runs into an &lt;code&gt;IndexError&lt;/code&gt; exception with the message &lt;code&gt;IndexError: list index out of range&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_list = [1, 2, 3, 4, 5, 6]
print(my_list[6])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    print(my_list[6])
IndexError: list index out of range
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if we try to access an element at a non-existent index in a string, the program runs into an &lt;code&gt;IndexError&lt;/code&gt; exception with the message &lt;code&gt;IndexError: string index out of range&lt;/code&gt;, as shown in the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;name = &amp;quot;Honeybadger&amp;quot;
print(name[20])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we tried to access the character at index 20 in the string. However, the string is eleven characters long. Hence, the program raises an &lt;code&gt;IndexError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    print(name[20])
IndexError: string index out of range
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key error&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;KeyError&lt;/code&gt; exceptions occur when we try to access a non-existent key in a Python dictionary. For instance, the dictionary in the following code has keys &lt;code&gt;&amp;quot;a&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;b&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;c&amp;quot;&lt;/code&gt;, and &lt;code&gt;&amp;quot;d&amp;quot;&lt;/code&gt;. When we try to fetch a value with the key &lt;code&gt;&amp;quot;e&amp;quot;&lt;/code&gt;, the program runs into a &lt;code&gt;KeyError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_dict = {&amp;quot;a&amp;quot;: 1, &amp;quot;b&amp;quot;: 2, &amp;quot;c&amp;quot;: 3, &amp;quot;d&amp;quot;: 4}
print(my_dict[&amp;quot;e&amp;quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    print(my_dict[&amp;quot;e&amp;quot;])
KeyError: &apos;e&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Module not found error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ModuleNotFoundError&lt;/code&gt; occurs when we try to import a module that hasn&apos;t already been installed or downloaded to the Python module search path.&lt;/p&gt;
&lt;p&gt;For example, suppose you want to use &lt;a href=&quot;https://docs.honeybadger.io/lib/python/integrations/other/&quot;&gt;Honeybadger for error monitoring in a Python application&lt;/a&gt;. However, if you don&apos;t &lt;a href=&quot;https://pypi.org/project/honeybadger/&quot;&gt;install honeybadger using pip&lt;/a&gt; and start directly by importing the &lt;code&gt;honeybadger&lt;/code&gt; module into your code, the program will run into &lt;code&gt;ModuleNotFoundError&lt;/code&gt; with the message &lt;code&gt;ModuleNotFoundError: No module named &apos;honeybadger&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import honeybadger
print(&amp;quot;You are at Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    import honeybadger
ModuleNotFoundError: No module named &apos;honeybadger&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ModuleNotFoundError&lt;/code&gt; is a specific type of &lt;code&gt;ImportError&lt;/code&gt; that occurs when Python cannot find the module file being imported.&lt;/p&gt;
&lt;h3&gt;Import errors in Python&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ImportError&lt;/code&gt; is a more general exception that can occur for various reasons during the import process, even when the module file exists but cannot be imported successfully due to dependency requirements or other issues. If a module exists but raises an exception while being imported, Python raises &lt;code&gt;ImportError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance, if we try to import a non-existent function from the &lt;code&gt;honeybadger&lt;/code&gt; module after installing it, the program runs into an &lt;code&gt;ImportError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from honeybadger import nonexistingfunction
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we tried to import &lt;code&gt;nonexistingfunction&lt;/code&gt; from the &lt;code&gt;honeybadger&lt;/code&gt; module. Hence, the program raises an &lt;code&gt;ImportError&lt;/code&gt; with the message &lt;code&gt;ImportError: cannot import name &apos;nonexistingfunction&apos; from &apos;honeybadger&apos;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    from honeybadger import nonexistingfunction
ImportError: cannot import name &apos;nonexistingfunction&apos; from &apos;honeybadger&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Attribute error&lt;/h3&gt;
&lt;p&gt;In Python, every object has a set of associated attributes, i.e., field names and methods. For example, a Python list has the &lt;code&gt;append()&lt;/code&gt; method that we use to add new values to a list. However, a tuple, an integer, a string, or a floating-point value doesn&apos;t have the &lt;code&gt;append()&lt;/code&gt; method. Hence, if we invoke the &lt;code&gt;append()&lt;/code&gt; method on a tuple, the program raises an &lt;code&gt;AttributeError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_tuple = (1, 2, 3, 4, 5)
my_tuple.append(6)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we have used the &lt;code&gt;append()&lt;/code&gt; method on a tuple. Hence, the program throws an &lt;code&gt;AttributeError&lt;/code&gt; exception with the message &lt;code&gt;AttributeError: &apos;tuple&apos; object has no attribute &apos;append&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    my_tuple.append(6)
AttributeError: &apos;tuple&apos; object has no attribute &apos;append&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Memory error&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MemoryError&lt;/code&gt; in Python is a built-in exception that occurs when a program fails to allocate memory for a Python object. It usually occurs when handling extremely large datasets, constructing oversized data structures, or running inefficient code that leads to excessive memory consumption or memory leaks.&lt;/p&gt;
&lt;p&gt;For example, creating a huge list having ten billion elements can lead to a &lt;code&gt;MemoryError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_list = [10] * (10**10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    my_list = [10] * (10**10)
MemoryError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While &lt;code&gt;MemoryError&lt;/code&gt; is a runtime error, it often indicates that the program has exceeded a system limit, such as running out of RAM or exceeding the operating system&apos;s address space limit.&lt;/p&gt;
&lt;h3&gt;Recursion error&lt;/h3&gt;
&lt;p&gt;A recursion error is a runtime error that occurs when the recursion depth exceeds the limit of 1000 recursive calls. The &lt;code&gt;RecursionError&lt;/code&gt; exception occurs if we forget to add a base case or a terminating condition while defining a recursive function. For instance, consider the following &lt;code&gt;increment_till_hundred()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def increment_till_hundred(x):
    x += 1
    print(x)
    increment_till_hundred(x)
    
increment_till_hundred(80)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;code&gt;increment_till_hundred()&lt;/code&gt; function, we haven&apos;t defined any termination condition if the value of &lt;code&gt;x&lt;/code&gt; reaches 100. Hence, the function keeps making the recursive call, exceeding the limit of 1000 recursive calls, and the program runs into &lt;code&gt;RecursionError&lt;/code&gt; with the message &lt;code&gt;RecursionError: maximum recursion depth exceeded while calling a Python object&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 6, in &amp;lt;module&amp;gt;
    increment_till_hundred(80)
  File &amp;quot;/path/to/code.py&amp;quot;, line 4, in increment_till_hundred
    increment_till_hundred(x)
  [Previous line repeated 994 more times]
  File &amp;quot;/path/to/code.py&amp;quot;, line 3, in increment_till_hundred
    print(x)
RecursionError: maximum recursion depth exceeded while calling a Python object
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to avoid runtime errors in Python?&lt;/h3&gt;
&lt;p&gt;Runtime errors are difficult to detect because they do not prevent the program from starting execution, unlike syntax errors. Hence, the program runs normally at first, and the error may only appear later when the line containing the problematic code is executed. To avoid runtime errors, you can use the following best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always validate the input data before processing to ensure it has the correct data type, format, and range.&lt;/li&gt;
&lt;li&gt;Always &lt;a href=&quot;https://www.honeybadger.io/blog/a-guide-to-exception-handling-in-python/&quot;&gt;implement exception handling in your Python code&lt;/a&gt; to catch and handle runtime errors gracefully, rather than crashing the program.&lt;/li&gt;
&lt;li&gt;Perform type checking using the &lt;code&gt;isinstance()&lt;/code&gt; function or convert the data type of values using &lt;code&gt;int()&lt;/code&gt;, &lt;code&gt;float()&lt;/code&gt;, and &lt;code&gt;str()&lt;/code&gt; functions before applying operations on values.&lt;/li&gt;
&lt;li&gt;Maintain a properly configured runtime environment, ensuring all required modules, dependencies, and system libraries are correctly installed.&lt;/li&gt;
&lt;li&gt;Test the code with different edge cases to identify potential runtime failures early.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apart from the above practices, always write modular, well-structured code so you can easily isolate and debug errors if they occur.&lt;/p&gt;
&lt;h2&gt;System-level errors in Python&lt;/h2&gt;
&lt;p&gt;System-level errors occur in a Python program when it encounters issues such as I/O failures, memory overflows, or connection errors. All the system-level errors in Python are raised using the &lt;code&gt;OSError&lt;/code&gt; exception or its subclasses.  Let&apos;s discuss the different system-level errors in Python and how to avoid them.&lt;/p&gt;
&lt;h3&gt;File not found errors in Python&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;FileNotFoundError&lt;/code&gt; exception occurs when we try to read a non-existent file. For example, suppose that we want to read a text file named &lt;code&gt;sampletextfile.txt&lt;/code&gt; using the &lt;code&gt;open()&lt;/code&gt; function. If the file doesn&apos;t exist, the program runs into &lt;code&gt;FileNotFoundError&lt;/code&gt; with the message &lt;code&gt;FileNotFoundError: [Errno 2] No such file or directory: &apos;sampletextfile.txt&apos;&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;file = open(&amp;quot;sampletextfile.txt&amp;quot;, &amp;quot;r&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    file = open(&amp;quot;sampletextfile.txt&amp;quot;, &amp;quot;r&amp;quot;)
FileNotFoundError: [Errno 2] No such file or directory: &apos;sampletextfile.txt&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Permission error&lt;/h3&gt;
&lt;p&gt;If a file exists and we don&apos;t have permission to read or modify it, the program raises a &lt;code&gt;PermissionError&lt;/code&gt; exception. For example, suppose that we have a file &lt;code&gt;samplefile.txt&lt;/code&gt; with only read access, as shown in the image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/errors-in-python/samplefile_permissions.png&quot; alt=&quot;Image showing permissions for samplefile.txt&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, if we try to open the file in &lt;code&gt;append&lt;/code&gt; mode and modify it, the program raises a &lt;code&gt;PermissionError&lt;/code&gt; exception with the message &lt;code&gt;PermissionError: [Errno 13] Permission denied: &apos;samplefile.txt&apos;&lt;/code&gt;. However, opening the file in read mode will not cause any issues, as we have permission to read it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;file = open(&amp;quot;samplefile.txt&amp;quot;, &amp;quot;a&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    file = open(&amp;quot;samplefile.txt&amp;quot;, &amp;quot;a&amp;quot;)
PermissionError: [Errno 13] Permission denied: &apos;samplefile.txt&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Is a directory error&lt;/h3&gt;
&lt;p&gt;We can open files using the &lt;code&gt;open()&lt;/code&gt; function in Python. However, if you try to open a directory using the &lt;code&gt;open()&lt;/code&gt; function, the program raises the &lt;code&gt;IsADirectoryError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;file = open(&amp;quot;/path/to/directory&amp;quot;, &amp;quot;r&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, &lt;code&gt;/path/to&lt;/code&gt; is a directory. Hence, the program throws an &lt;code&gt;IsADirectoryError&lt;/code&gt; exception with the message &lt;code&gt;IsADirectoryError: [Errno 21] Is a directory&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    file = open(&amp;quot;/path/to/directory&amp;quot;, &amp;quot;r&amp;quot;)
IsADirectoryError: [Errno 21] Is a directory: &apos;/path/to/directory&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connection error&lt;/h3&gt;
&lt;p&gt;In Python, the &lt;code&gt;ConnectionError&lt;/code&gt; exception occurs due to network issues such as a lost internet connection, DNS errors, server downtime, or when the client fails to establish a connection to the server within a specified time limit. For instance, using the &lt;code&gt;requests&lt;/code&gt; module to make an API call without connecting the system to the network results in the &lt;code&gt;ConnectionError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import requests
response = requests.get(&apos;https://jsonplaceholder.typicode.com/todos/1&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 4, in &amp;lt;module&amp;gt;
    response = requests.get(&apos;https://jsonplaceholder.typicode.com/todos/1&apos;)
  .
  .
  File &amp;quot;/home/user/.local/lib/python3.10/site-packages/requests/adapters.py&amp;quot;, line 700, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host=&apos;jsonplaceholder.typicode.com&apos;, port=443): Max retries exceeded with url: /todos/1 (Caused by NameResolutionError(&amp;quot;&amp;lt;urllib3.connection.HTTPSConnection object at 0x7400d45a6c80&amp;gt;: Failed to resolve &apos;jsonplaceholder.typicode.com&apos; ([Errno -3] Temporary failure in name resolution)&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connection refused error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ConnectionRefusedError&lt;/code&gt; is a subclass of &lt;code&gt;ConnectionError&lt;/code&gt;, which specifically indicates that a connection attempt was explicitly refused by the remote host. It occurs due to an incorrect IP address or port number, firewall blocking, or if the server is not running or has reached its maximum capacity for pending connections. For example, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((&amp;quot;localhost&amp;quot;, 9999))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have not run any application on port 9999. Hence, when the Python program tries to connect to the port, the program runs into &lt;code&gt;ConnectionRefusedError&lt;/code&gt; with the message &lt;code&gt;ConnectionRefusedError: [Errno 111] Connection refused&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 3, in &amp;lt;module&amp;gt;
    s.connect((&amp;quot;localhost&amp;quot;, 9999))
ConnectionRefusedError: [Errno 111] Connection refused
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to avoid system-level errors in Python?&lt;/h3&gt;
&lt;p&gt;System-level errors in Python are often caused by missing files, insufficient permissions, memory limitations, or network failures. Although we cannot always prevent them, we can minimize system-level errors through careful resource management, validation checks, and proper exception handling. You can use the following practices to avoid system-level errors in Python.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always check whether files and directories exist before performing file operations.&lt;/li&gt;
&lt;li&gt;Check access permissions to ensure the program has the required read, write, and execute permissions for files and directories.&lt;/li&gt;
&lt;li&gt;Manage memory efficiently by avoiding extremely large data structures and using generators or batch processing for large datasets.&lt;/li&gt;
&lt;li&gt;Use context managers (&lt;code&gt;with&lt;/code&gt; statement) when working with files, sockets, or other resources to ensure they are automatically closed after use.&lt;/li&gt;
&lt;li&gt;Ensure required system resources, such as disk space, memory, and network connectivity, are available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best way to avoid system-level errors is to anticipate potential failures and write defensive code that validates resources and handles exceptions gracefully, since these are mostly caused by environmental or resource constraints.&lt;/p&gt;
&lt;h2&gt;Logical errors in Python&lt;/h2&gt;
&lt;p&gt;A logical or semantic error in Python occurs when a program runs without crashing or raising exceptions but produces incorrect or unintended results due to flawed code logic. Unlike syntax errors or runtime errors, Python cannot detect logical errors automatically because the code is syntactically valid and executes successfully. Logical errors occur due to incorrect algorithms, wrong conditions, faulty calculations, or mistaken assumptions in program logic.&lt;/p&gt;
&lt;p&gt;For example, consider that you are writing a Python application to determine the voting eligibility of a person based on their age. It is given that a person aged 18 or older is eligible to vote. Now consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if age &amp;gt; 18:
    print(&amp;quot;Eligible to vote&amp;quot;)
else:
    print(&amp;quot;Not eligible&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code executes successfully. But it produces incorrect output for people aged 18 due to an incorrect condition. Hence, we should have used the &lt;code&gt;&amp;gt;=&lt;/code&gt; operator instead of the &lt;code&gt;&amp;gt;&lt;/code&gt; operator in the if block.&lt;/p&gt;
&lt;p&gt;Logical errors can also occur due to incorrect use of logical operators. For example, suppose we need to determine whether a person is of working age, defined as 18 to 60 years old, inclusive. Now, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;age = 70
if age &amp;gt;= 18 or age &amp;lt;= 60:
    print(&amp;quot;Working age&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Working age
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code prints &lt;code&gt;&amp;quot;Working age&amp;quot;&lt;/code&gt; even if the person is over 60 years old because the first condition is True, and the &lt;code&gt;or&lt;/code&gt; operator evaluates to True if either operand is True. Hence, we should have used the &lt;code&gt;and&lt;/code&gt; operator rather than the &lt;code&gt;or&lt;/code&gt; operator to get the correct output.&lt;/p&gt;
&lt;p&gt;Logical errors are much harder to detect because the Python application does not run into exceptions. The output may appear reasonable but will still be incorrect. We can detect and avoid logical errors through &lt;a href=&quot;https://www.honeybadger.io/blog/beginners-guide-to-software-testing-in-python/&quot;&gt;unit testing&lt;/a&gt;, debugging, and code review, ensuring there are no flaws in the code.&lt;/p&gt;
&lt;h2&gt;Know your errors before your users do&lt;/h2&gt;
&lt;p&gt;No matter how experienced you get, errors in an application never fully disappear. What changes is how quickly you recognize them, how calmly you respond, and how efficiently you solve the errors. In this article, we discussed the different syntax errors, runtime errors, system-level errors, and logical errors in Python and how to avoid them. While we cannot eliminate errors entirely, we can significantly reduce them by following &lt;a href=&quot;https://www.honeybadger.io/blog/fastapi-error-handling/&quot;&gt;error-handling best practices&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is important to recognize that not every error can be caught during development. Certain bugs only emerge in production, often triggered by specific user actions in unpredictable sequences that may not be covered by testing. When this occurs, users may encounter an error before the development team discovers it. This is where error tracking and application monitoring become essential.&lt;/p&gt;
&lt;p&gt;Honeybadger provides error tracking, logging, uptime monitoring, and performance monitoring under one roof. &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;Sign up for a free trial of Honeybadger&lt;/a&gt; to monitor your applications by combining error tracking, logging, and uptime monitoring, so you always know the state of your application and can catch errors before they snowball.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Heroku vs AWS</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/heroku-vs-aws/"/>
    <id>https://www.honeybadger.io/blog/heroku-vs-aws/</id>
    <published>2026-04-21T00:00:00+00:00</published>
    <updated>2026-04-21T00:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">Deciding between Heroku&apos;s streamlined platform and AWS&apos;s infrastructure control shapes your deployment strategy and operational overhead. This comprehensive comparison explores architecture patterns, pricing models, deployment complexity, and migration considerations to help engineers choose the right hosting approach for their applications.</summary>
    <content type="html">&lt;p&gt;Heroku vs AWS: these cloud platforms represent fundamentally different approaches to application cloud hosting. The decision between them often determines whether your team ships features in hours or spends days configuring infrastructure. Both platforms represent different philosophies in cloud computing, with Heroku prioritizing developer experience while AWS maximizes infrastructure control. Modern applications rely on technologies called cloud computing to deliver scalable, on-demand infrastructure without maintaining physical servers.&lt;/p&gt;
&lt;p&gt;This Heroku vs AWS comparison examines the factors that impact your choice: deployment workflows (Git-push simplicity versus infrastructure configuration), cost structures (transparent per-dyno pricing versus compounded service charges), scaling approaches (instant dyno multiplication versus auto-scaling policies), operational overhead (managed updates versus manual maintenance), and migration complexity (platform lock-in versus portable architectures).&lt;/p&gt;
&lt;h2&gt;Heroku vs AWS: things to consider&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/heroku-vs-aws/heroku-aws-components.png&quot; alt=&quot;Heroku vs AWS: Components comparison&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Heroku platform operates as an opinionated PaaS platform built atop AWS infrastructure, abstracting away server management and underlying infrastructure completely. The platform&apos;s containers equivalent on Heroku are called dynos, which handle application processes. It runs on lightweight Linux containers that boot from a slug (compiled application bundle). Each dyno receives 512MB of memory in the cheapest tier and scales to multiple gigabytes in performance tiers. The filesystem is ephemeral, which resets with each dyno restart. This forces stateless application design and external storage for persistent data.&lt;/p&gt;
&lt;p&gt;As an AWS IaaS platform, AWS equips developers with discrete building blocks like EC2 instances, Lambda functions, ECS containers, RDS databases, Amazon Simple Storage Service (or Amazon S3) buckets, that developers assemble into custom architectures and complex infrastructure. An EC2 t3.micro instance provides 1GB of memory and two vCPUs with full filesystem access and persistent storage via EBS volumes. Lambda executes functions in response to events with 128MB to 10GB configurable memory. AWS functions as a comprehensive cloud service provider offering over 200 services, while Heroku operates as a focused PaaS layer.&lt;/p&gt;
&lt;p&gt;This enables architectures ranging from monolithic EC2 deployments to serverless microservices, but shifts infrastructure decisions to development teams. AWS Elastic Compute Cloud (EC2) provides the foundational infrastructure that many cloud services build upon, including Heroku itself.&lt;/p&gt;
&lt;p&gt;Heroku&apos;s infrastructure buildpack system detects application programming languages and automatically configures runtime environments. A Node.js application with a &lt;code&gt;package.json&lt;/code&gt; triggers the Node.js buildpack, which installs dependencies, compiles assets, and prepares the execution environment. AWS offers similar automation through Elastic Beanstalk, but most DevOps engineers (AWS developers) construct custom deployment pipelines using CloudFormation templates or Terraform configurations that explicitly define every resource.&lt;/p&gt;
&lt;p&gt;The networking models differ substantially. Heroku applications receive an &lt;code&gt;*.herokuapp.com&lt;/code&gt; subdomain by default, with routing managed transparently through the platform&apos;s load balancer. Custom domains require DNS configuration, but no manual load balancer setup.&lt;/p&gt;
&lt;p&gt;AWS, on the other hand, requires explicit virtual private cloud (VPC) configuration, subnet allocation, security settings through security group rules, elastic load balancing through load balancer provisioning, and target group configuration before applications become accessible on the internet. Engineers who manage AWS infrastructure spend significant time in the AWS Management Console configuring VPCs, security groups, and access management policies.&lt;/p&gt;
&lt;h2&gt;Deployment workflows: Git-push simplicity vs infrastructure configuration&lt;/h2&gt;
&lt;p&gt;For deploying an app, Heroku centers on Git-based workflows with minimal configuration. Developers push code to a Heroku Git remote, triggering an automatic build process that creates a new slug, deploys it across dynos, and performs health checks before routing traffic. The entire sequence completes without manual intervention:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Initial Heroku setup
heroku create myapp
heroku addons:create heroku-postgresql:essential-0

# Deploy with a single command
git push heroku main

# View application logs
heroku logs --tail

# Scale application
heroku ps:scale web=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Heroku&apos;s intuitive Heroku platform dashboard allows developers to manage deployments, scale dynos, and &lt;a href=&quot;https://docs.honeybadger.io/lib/ruby/integration-guides/heroku-exception-tracking/&quot;&gt;monitor applications&lt;/a&gt; without deep cloud computing knowledge. The platform handles dependency resolution, asset compilation, and process management based on the Procfile specification, enabling rapid app development without manual server configuration. A typical Node.js application requires only a &lt;code&gt;Procfile&lt;/code&gt; defining process types:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;web: node server.js
worker: node worker.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Heroku&apos;s dyno manager reads this configuration and manages process lifecycles. Database credentials, API keys, and configuration values pass through environment variables accessible via &lt;code&gt;process.env&lt;/code&gt;, with the platform injecting connection strings for provisioned add-ons.&lt;/p&gt;
&lt;p&gt;AWS services require explicit infrastructure definition and AWS environment setup before code deployment, introducing the AWS learning curve that teams must navigate. Using AWS Elastic Beanstalk provides the closest analog to Heroku&apos;s experience, but even this managed service demands configuration files defining environment settings, instance types, and scaling parameters. A minimal Elastic Beanstalk configuration in &lt;code&gt;.ebextensions/nodecommands.config&lt;/code&gt; will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;option_settings:
  aws:elasticbeanstalk:container:nodejs:
    NodeCommand: &amp;quot;node server.js&amp;quot;

  aws:autoscaling:launchconfiguration:
    InstanceType: t3.small

  aws:elasticbeanstalk:environment:
    EnvironmentType: LoadBalanced
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deployment proceeds through the AWS powerful CLI or EB CLI, which packages the application, uploads it to S3, and updates the environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Initialize Elastic Beanstalk
eb init -p node.js-18 myapp

# Create environment with database
eb create myapp-prod --database.engine postgres --database.size 20

# Deploy application
eb deploy

# Access logs
eb logs

# Scale instances
eb scale 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to bypass Elastic Beanstalk, you would have to construct deployment pipelines manually. A common pattern uses AWS CodePipeline to orchestrate source control integration, CodeBuild for compilation, and ECS for container orchestration.&lt;/p&gt;
&lt;p&gt;Container-based AWS deployments using ECS Fargate require a task definition JSON specifying container images, CPU and memory allocation, environment variables, and networking configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;family&amp;quot;: &amp;quot;myapp&amp;quot;,
  &amp;quot;networkMode&amp;quot;: &amp;quot;awsvpc&amp;quot;,
  &amp;quot;requiresCompatibilities&amp;quot;: [&amp;quot;FARGATE&amp;quot;],
  &amp;quot;cpu&amp;quot;: &amp;quot;256&amp;quot;,
  &amp;quot;memory&amp;quot;: &amp;quot;512&amp;quot;,
  &amp;quot;containerDefinitions&amp;quot;: [
    {
      &amp;quot;name&amp;quot;: &amp;quot;web&amp;quot;,
      &amp;quot;image&amp;quot;: &amp;quot;123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest&amp;quot;,
      &amp;quot;portMappings&amp;quot;: [
        {
          &amp;quot;containerPort&amp;quot;: 3000,
          &amp;quot;protocol&amp;quot;: &amp;quot;tcp&amp;quot;
        }
      ],
      &amp;quot;environment&amp;quot;: [
        {
          &amp;quot;name&amp;quot;: &amp;quot;DATABASE_URL&amp;quot;,
          &amp;quot;value&amp;quot;: &amp;quot;postgresql://user:pass@db.example.com:5432/dbname&amp;quot;
        }
      ],
      &amp;quot;logConfiguration&amp;quot;: {
        &amp;quot;logDriver&amp;quot;: &amp;quot;awslogs&amp;quot;,
        &amp;quot;options&amp;quot;: {
          &amp;quot;awslogs-group&amp;quot;: &amp;quot;/ecs/myapp&amp;quot;,
          &amp;quot;awslogs-region&amp;quot;: &amp;quot;us-east-1&amp;quot;,
          &amp;quot;awslogs-stream-prefix&amp;quot;: &amp;quot;ecs&amp;quot;
        }
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This task definition connects to a service definition that integrates with Application Load Balancers, configures health checks, and manages deployment strategies (rolling updates, blue-green deployments). The manual assembly process provides control over resource allocation but increases deployment complexity by an order of magnitude compared to Heroku&apos;s Git-push model.&lt;/p&gt;
&lt;h2&gt;Cost models and pricing analysis&lt;/h2&gt;
&lt;p&gt;Heroku vs AWS pricing comparison may not be as straightforward as you might think. The choice between these cloud platforms ultimately depends on whether teams value deployment simplicity or infrastructure flexibility. Heroku&apos;s pricing follows a straightforward per-dyno model. The Basic plan charges $7 monthly per dyno, providing 512MB memory and automatic SSL. Standard dynos cost $25-50 monthly with 512MB-1GB memory. Performance dynos range from $250-500 monthly for 2.5GB-14GB memory. Add-ons follow similar transparent pricing. Heroku Postgres Essential-0 costs $5 monthly for 1GB storage and 20 connections, while Essential-1 provides 10GB storage for $9 monthly.&lt;/p&gt;
&lt;p&gt;AWS pricing compounds multiple service charges. AWS instance pricing starts at approximately $15 monthly for an EC2 t3.small instance (2 vCPUs, 2GB memory) in us-east-1 with on-demand pricing, but requires additional charges for EBS storage ($0.10 per GB-month), data transfer ($0.09 per GB egress), and load balancer operation ($16 monthly base plus $0.008 per LCU-hour). While AWS offers free tiers for experimentation (750 hours of t2.micro monthly), reserved instances reduce EC2 costs by 40-60% with one or three-year commitments for production workloads, introducing capacity planning complexity we don&apos;t have in Heroku&apos;s monthly subscriptions.&lt;/p&gt;
&lt;p&gt;Consider a Node.js/React application serving 10,000 monthly active users with moderate traffic patterns. The architecture includes a web server, background worker, PostgreSQL database, and Redis cache:&lt;/p&gt;
&lt;p&gt;Heroku deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2x Standard-1X web dynos (1GB each): $50/month&lt;/li&gt;
&lt;li&gt;1x Standard-1X worker dyno: $25/month&lt;/li&gt;
&lt;li&gt;Heroku Postgres Standard-0 (64GB, 120 connections): $50/month&lt;/li&gt;
&lt;li&gt;Heroku Redis Premium-0 (100MB): $15/month&lt;/li&gt;
&lt;li&gt;Total: $140/month&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AWS deployment (Elastic Beanstalk):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2x t3.small EC2 instances: $30/month&lt;/li&gt;
&lt;li&gt;Application Load Balancer: $20/month (estimated with LCU charges)&lt;/li&gt;
&lt;li&gt;RDS PostgreSQL db.t3.micro (1 vCPU, 1GB, 20GB storage): $25/month&lt;/li&gt;
&lt;li&gt;ElastiCache Redis cache.t3.micro (0.5GB): $12/month&lt;/li&gt;
&lt;li&gt;EBS storage (20GB across instances): $2/month&lt;/li&gt;
&lt;li&gt;Data transfer (estimated 50GB egress): $4.50/month&lt;/li&gt;
&lt;li&gt;Total: $93.50/month&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AWS deployment (ECS Fargate):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2x Fargate tasks (0.5 vCPU, 1GB each, continuous): $36/month&lt;/li&gt;
&lt;li&gt;Application Load Balancer: $20/month&lt;/li&gt;
&lt;li&gt;RDS PostgreSQL db.t3.micro: $25/month&lt;/li&gt;
&lt;li&gt;ElastiCache Redis cache.t3.micro: $12/month&lt;/li&gt;
&lt;li&gt;Data transfer: $4.50/month&lt;/li&gt;
&lt;li&gt;Total: $97.50/month&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Organizations leveraging cloud computing services from either provider must consider long-term operational costs beyond monthly subscription fees. Heroku&apos;s simplified pricing will eliminate surprise charges from misconfigured computing resources, and there will be no accidental fees. AWS requires constant cost monitoring through Cost Explorer, budget alerts, and tagging strategies to prevent billing surprises.&lt;/p&gt;
&lt;p&gt;Note: It may look like you will spend less on AWS, but the extra technical expertise required to make AWS run as expected might increase the overall expense for deployment in the long run.&lt;/p&gt;
&lt;h2&gt;Deployment features comparison&lt;/h2&gt;
&lt;p&gt;Heroku and AWS offer overlapping capabilities with vastly different implementation approaches. The following table compares deployment features across Heroku and three common AWS deployment patterns (Elastic Beanstalk (managed PaaS), ECS Fargate (container orchestration), and direct EC2 management).&lt;/p&gt;
&lt;p&gt;Features marked &amp;quot;Manual&amp;quot; require custom implementation through scripts, configuration management tools, or third-party services rather than automation provided by the platform.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/heroku-vs-aws/platform-comparison-heroku-aws.png&quot; alt=&quot;A comparison of features between Heroku and AWS platforms&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Scaling AWS vs Heroku: performance and operational tradeoffs&lt;/h2&gt;
&lt;p&gt;Heroku and AWS manage cloud infrastructure differently, as we know. Heroku hides complexity behind automation while AWS demands explicit configuration at every level. Scaling Heroku applications works through dyno multiplication for horizontal scaling (launching multiple app instances) and dyno type selection for vertical scaling. Scaling to 10 Standard-2X dynos (creating multiple app instances of 1GB each) happens instantly via the CLI or dashboard without application code changes. The platform&apos;s intelligent routing distributes requests across available dynos using a routing algorithm that occasionally produces request queuing under heavy load. Performance-M dynos provide dedicated computing resources and reduced request latency compared to Standard dynos&apos; shared infrastructure.&lt;/p&gt;
&lt;p&gt;AWS auto-scaling operates at the infrastructure layer. Elastic Beanstalk configurations specify scaling triggers based on CloudWatch metrics. A typical configuration scales EC2 instances when the average CPU exceeds 70% for five minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;option_settings:
  aws:autoscaling:asg:
    MinSize: 2
    MaxSize: 10

  aws:autoscaling:trigger:
    MeasureName: CPUUtilization
    Statistic: Average
    Unit: Percent
    UpperThreshold: 70
    UpperBreachScaleIncrement: 2
    LowerThreshold: 30
    LowerBreachScaleIncrement: -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ECS Fargate enables finer-grained scaling through target tracking policies on custom CloudWatch metrics. Teams emit application-level metrics (active connections, queue depth, response times) to CloudWatch and configure scaling policies responding to business logic rather than infrastructure metrics. This scaling responds to load patterns that infrastructure metrics miss.&lt;/p&gt;
&lt;p&gt;Lambda functions scale automatically to concurrent execution limits (1,000 concurrent invocations by default, increasable via service quotas). Each invocation runs in an isolated execution context with dedicated memory and CPU allocation. Cold start latency (100-800ms for Node.js functions) impacts performance for infrequently accessed endpoints but disappears under sustained load as Lambda maintains warm execution contexts.&lt;/p&gt;
&lt;p&gt;Heroku&apos;s ephemeral filesystem and 30-second request timeout constrain certain workload types. Long-running background jobs exceeding 30 seconds require worker dynos rather than web dynos. File uploads must stream directly to S3 or similar external storage since filesystem writes disappear on dyno restart.&lt;/p&gt;
&lt;p&gt;AWS imposes no such timeout restrictions. EC2 and ECS tasks run indefinitely, enabling long-running processes, persistent WebSocket connections, and stateful application architectures. The flexibility introduces operational responsibilities that Heroku manages automatically. Teams must implement health checks, graceful shutdown handlers, and restart policies that Heroku provides out of the box.&lt;/p&gt;
&lt;p&gt;Network performance follows similar patterns. Heroku&apos;s shared routing infrastructure introduces variable latency (typically 10-50ms) between request receipt and application processing. AWS Application Load Balancers add 1-5ms latency with predictable performance characteristics. Teams requiring single-digit millisecond response times deploy on AWS with dedicated load balancers, placement groups for reduced inter-instance latency, and Enhanced Networking for 25-100Gbps network bandwidth.&lt;/p&gt;
&lt;h2&gt;When to choose Heroku, when to choose AWS, and when to use both&lt;/h2&gt;
&lt;p&gt;Understanding the architectural differences between Heroku and AWS helps you select the right foundation for web apps, APIs, and mobile apps. Heroku suits teams prioritizing rapid app development velocity and a streamlined deployment process over infrastructure optimization, avoiding the AWS learning curve. Startups validating product-market fit, agencies shipping client projects rapidly, and small teams without dedicated operations staff benefit from Heroku&apos;s reduced operational overhead. The platform eliminates infrastructure decisions that distract from product development.&lt;/p&gt;
&lt;p&gt;Choose Heroku when building highly scalable web applications that fit platform constraints: stateless web applications, API servers, scheduled background jobs with reasonable duration, and traffic patterns under 100 requests per second. The 30-second request timeout handles most HTTP endpoints adequately. Applications requiring PostgreSQL, Redis, Kafka, Elasticsearch, or other common infrastructure components benefit from the managed services Heroku provides through add-ons with automatic backups, monitoring tools, and maintenance.&lt;/p&gt;
&lt;p&gt;AWS services become necessary when applications demand infrastructure flexibility beyond Heroku&apos;s abstractions, despite the steeper AWS learning curve for DevOps engineers. Machine learning inference workloads requiring GPU instances, real-time analytics processing terabytes daily, multi-region active-active deployments, or HIPAA/SOC 2 compliance with dedicated tenancy all exceed Heroku&apos;s capabilities. Teams optimizing infrastructure costs at scale achieve 50-70% savings through reserved instances, spot instances, and resource right-sizing, which Heroku&apos;s fixed pricing model prevents.&lt;/p&gt;
&lt;p&gt;Hybrid architectures combine both platforms strategically. A common pattern deploys the core web application on Heroku for rapid iteration while offloading specialized workloads to AWS, like video transcoding in AWS Lambda, data analytics in EMR, machine learning inference in SageMaker, and file storage in S3, all part of the broader AWS ecosystem. Heroku applications&apos; integration with AWS services happens through API endpoints or direct SDK calls using IAM credentials stored in Heroku config vars.&lt;/p&gt;
&lt;h2&gt;Migration considerations and common pitfalls&lt;/h2&gt;
&lt;p&gt;Migrating from Heroku to AWS requires addressing architectural assumptions baked into Heroku applications. Applications writing temporary files, caching assets locally, or storing session data on disk fail when moved to AWS without refactoring to Redis, S3, or database-backed storage.&lt;/p&gt;
&lt;p&gt;Environment variable management becomes explicit during AWS migration. Heroku injects DATABASE_URL and other add-on credentials automatically, while AWS requires manual configuration through Systems Manager Parameter Store, Secrets Manager, or environment variables in task definitions. The migration checklist must enumerate every config var and establish AWS equivalents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Export Heroku environment variables
heroku config --shell &amp;gt; .env.heroku

# Convert to AWS Systems Manager parameters
while IFS=&apos;=&apos; read -r key value; do
  aws ssm put-parameter \
    --name &amp;quot;/myapp/prod/$key&amp;quot; \
    --value &amp;quot;$value&amp;quot; \
    --type &amp;quot;SecureString&amp;quot;
done &amp;lt; .env.heroku
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Database migration requires careful planning around downtime windows and replication lag. The recommended approach establishes AWS RDS as a follower to Heroku using logical replication, allowing traffic cutover with minimal downtime.&lt;/p&gt;
&lt;p&gt;Once replication lag stabilizes (typically under one second), maintenance mode redirects traffic to AWS while the final database sync completes. The process works for databases under 100GB, but becomes impractical for terabyte-scale datasets requiring offline migration windows or more sophisticated replication tooling.&lt;/p&gt;
&lt;p&gt;Applications using Heroku add-ons, including review apps for testing branches, must replace each with AWS equivalents or third-party SaaS. Heroku Redis maps to ElastiCache, Heroku Kafka to Amazon MSK, Heroku Scheduler to CloudWatch Events + Lambda.&lt;/p&gt;
&lt;p&gt;DNS cutover represents the final migration step. Heroku applications respond to &lt;code&gt;myapp.herokuapp.com&lt;/code&gt; and custom domains through Heroku&apos;s edge routing. Amazon Web Services migrations require updating DNS records to point at CloudFront distributions, Application Load Balancers, or API Gateway endpoints.&lt;/p&gt;
&lt;h2&gt;Choosing the right platform for your team&lt;/h2&gt;
&lt;p&gt;This Heroku vs AWS comparison makes one thing clear: these cloud platforms aren&apos;t competitors&#x2014;they&apos;re cloud services built for different stages of a product&apos;s life, each with distinct approaches to cloud computing and infrastructure management. Heroku gives you the fastest path to a working, scalable application with almost no operational overhead, while Amazon Web Services offers the flexibility needed for complex architectures, large-scale optimizations, and workloads that exceed platform limits.&lt;/p&gt;
&lt;p&gt;DevOps engineers moving between the two must plan for changes in deployment workflows, environment management, and infrastructure responsibilities, especially when shedding Heroku&#x2019;s built-in conveniences. The comparison between Heroku and AWS extends beyond pricing to include developer productivity, operational overhead, and long-term scalability.&lt;/p&gt;
&lt;p&gt;Like this article? Join the &lt;a href=&quot;https://www.honeybadger.io/newsletter/&quot;&gt;Honeybadger newsletter&lt;/a&gt; to learn more.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Why every web developer should explore machine learning</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/machine-learning-for-web-developers/"/>
    <id>https://www.honeybadger.io/blog/machine-learning-for-web-developers/</id>
    <published>2020-03-18T00:00:00+00:00</published>
    <updated>2026-04-09T00:00:00+00:00</updated>
    <author>
      <name>Julie Kent</name>
    </author>
    <summary type="text">If software&apos;s been eating the world for the past twenty years, it&apos;s safe to say machine learning has been eating it for the past five. But what exactly is machine learning? Why should a web developer care? This article by Julie Kent answers these questions.</summary>
    <content type="html">&lt;p&gt;I don&apos;t have kids yet, but when I do, I want them to learn two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Personal finance&lt;/li&gt;
&lt;li&gt;Machine learning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether or not you believe that the singularity is near, there&apos;s no denying that the world runs on data. Understanding how that data is transformed into knowledge is critical for anyone coming of age these days &#x2013; and even more so for developers.&lt;/p&gt;
&lt;p&gt;This is the first article in a series that will attempt to make machine learning (ML) accessible to full-stack Ruby developers. By understanding the ML tools at your disposal, you&apos;ll be able to help your stakeholders make better decisions. Future articles will focus on individual techniques and practical examples, but in this one, we&apos;re setting the stage &#x2013; showing you a map and placing a pin that says &amp;quot;you are here.&amp;quot;&lt;/p&gt;
&lt;h2&gt;Humble Beginnings&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/machine-learning-for-web-developers/ml-series.png&quot; alt=&quot;Robot studying&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Artificial intelligence (AI) and machine learning are nothing new. Back in the 1950s, Arthur Samuel built a computer program that could play checkers. He used &amp;quot;alpha-beta pruning&amp;quot; &#x2013; a common search algorithm.&lt;/p&gt;
&lt;p&gt;The 1960s saw the advent of multi-layered neural networks and the nearest-neighbor algorithm, which is used to find optimal pathing in warehouses.&lt;/p&gt;
&lt;p&gt;So if AI is so old, why are AI startups so trendy? In my opinion, there are two reasons for this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Computing power (see &lt;a href=&quot;https://en.wikipedia.org/wiki/Moore%27s_law&quot;&gt;Moore&apos;s Law&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The amount of data being added to the internet every day&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are two statistics related to the amount of data being created on a daily basis that blow my mind every time I think about them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As of 2018, we are producing 2.5 quintillion bytes of data every day. No doubt this number has only increased since &lt;a href=&quot;https://www.forbes.com/sites/bernardmarr/2018/05/21/how-much-data-do-we-create-every-day-the-mind-blowing-stats-everyone-should-read/#71a45cb060ba&quot;&gt;this Forbes article&lt;/a&gt; was published.&lt;/li&gt;
&lt;li&gt;Over the last two years alone, 90% of the data in the world was generated.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, what this means is that (1) the hardware necessary to store data and run algorithms continues to become more affordable and (2) the amount of data available to train the ML models is increasing at a crazy-fast pace.&lt;/p&gt;
&lt;p&gt;Every day we are interacting with, being influenced by, and contributing to the world of artificial intelligence and machine learning. For example, you can thank (or blame) algorithms for the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.theverge.com/2019/11/11/20958953/apple-credit-card-gender-discrimination-algorithms-black-box-investigation&quot;&gt;Your credit line&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Helping &lt;a href=&quot;https://www.entrepreneur.com/article/341626&quot;&gt;diagnose illness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Maybe even whether or not you &lt;a href=&quot;https://www.pbs.org/newshour/economy/making-sense/how-using-facial-recognition-in-job-interviews-could-reinforce-inequality&quot;&gt;got the job&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Helping you find the most efficient route given current traffic conditions&lt;/li&gt;
&lt;li&gt;Alexa understanding exactly what you mean when you tell her you just sneezed&lt;/li&gt;
&lt;li&gt;Spotify introducing you to your new favorite song&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason I brought up my hypothetical future kids is this: I want them to understand how their digital lives influence their &amp;quot;real&amp;quot; lives, the implications of their data privacy decisions, and how to form their own opinions of when they should trust the machine vs. when they shouldn&apos;t.&lt;/p&gt;
&lt;p&gt;In the remainder of this post, I want to give an overview of the three kinds of machine learning that I&apos;ve studied: supervised learning, unsupervised learning, and reinforcement learning. We&apos;ll talk about what makes each approach unique and the problems each is especially good at solving.&lt;/p&gt;
&lt;h2&gt;Supervised Learning&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/machine-learning-for-web-developers/ml-1.png&quot; alt=&quot;Supervised learning diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Supervised learning is... well, supervised by humans. :) Imagine that we&apos;re building a supervised learning system to decide who gets approved for a mortgage. Here&apos;s how it might work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The bank compiles a dataset that maps customer attributes (age, salary, etc.) to outcomes (repayment, default, etc.).&lt;/li&gt;
&lt;li&gt;We train our system using the data.&lt;/li&gt;
&lt;li&gt;The system uses what it learns to guess future outcomes based on an applicant&apos;s attributes.&lt;/li&gt;
&lt;li&gt;If the algorithm guesses correctly, we tell it, &amp;quot;Great job! You&apos;re right.&amp;quot; But if it&apos;s wrong, we tell it, &amp;quot;No, you&apos;re incorrect. Please improve and try again.&amp;quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This example is considered a &amp;quot;classification&amp;quot; problem because the output of the algorithm is a category &#x2013; in this case, approved or not approved. Other examples of classification problems include deciding whether or not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a person has an illness&lt;/li&gt;
&lt;li&gt;an x-ray has a broken bone&lt;/li&gt;
&lt;li&gt;an e-mail is spam&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re curious to learn more about the math behind ML classification algorithms, Google any of these: naive Bayes classifiers, support vector machines, logistic regression, neural networks, random forests.&lt;/p&gt;
&lt;p&gt;In addition to classification problems that render a &amp;quot;yes/no&amp;quot; outcome, supervised learning can also be utilized to solve regression problems &#x2013; here, we make a prediction on a continuous scale, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the future value of a stock&lt;/li&gt;
&lt;li&gt;the probability that the New England Patriots win the Super Bowl&lt;/li&gt;
&lt;li&gt;the average salary a company needs to offer a candidate for them to accept&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Examples of algorithms used for supervised regression problems include linear regression, non-linear regression, and Bayesian linear regression.&lt;/p&gt;
&lt;h2&gt;Unsupervised Learning&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/machine-learning-for-web-developers/ml-2.png&quot; alt=&quot;Unsupervised learning diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With our supervised learning example, we predefined the classification categories. The mortgage applicant was either approved or denied.&lt;/p&gt;
&lt;p&gt;With unsupervised learning, we don&apos;t provide the categories. They&apos;re not available to us. The algorithm must come up with its own conclusions.&lt;/p&gt;
&lt;p&gt;Why would we want to use an unsupervised approach?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Sometimes we don&apos;t know categories beforehand. Much of the data floating around the internet is unstructured &#x2013; i.e., lacking labels.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Other times, we don&apos;t know what we&apos;re looking for, so we can ask the algorithm to find patterns/features that can be useful for categorization.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Another way to handle unstructured data with machine learning is to simply have humans look at it and manually label it. There are lots of companies that hire workers to manually classify data: &lt;a href=&quot;https://www.ft.com/content/56dde36c-aa40-11e9-984c-fac8325aaa04&quot;&gt;labeling data&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Approaches to Unsupervised Learning&lt;/h2&gt;
&lt;p&gt;Two techniques that are commonly used in unsupervised learning are &lt;em&gt;association and clustering&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Association:&lt;/strong&gt; Imagine that you&apos;re Amazon. You have a lot of customer data, purchase history, etc. By using unsupervised learning, you could partition customers into &amp;quot;types of shoppers&amp;quot; &#x2013; maybe figuring out that those who buy pink umbrellas are more likely to also purchase Matcha tea.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clustering:&lt;/strong&gt; Clustering looks at your data and partitions it into a specified number of groups, or clusters. For example, maybe you have a bunch of housing data and you want to see if there are any features (possibly crime data?) that can predict what neighborhood the home is in. Or, techniques like cosine similarity can be used for text classification (e.g., is this article about tennis, cooking, or space?).&lt;/p&gt;
&lt;p&gt;If you&apos;re interested in learning more about specific unsupervised learning techniques, Google search k-means clustering, cosine similarity, hierarchical clustering, and k-nearest-neighbor clustering.&lt;/p&gt;
&lt;h2&gt;Reinforcement Learning&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/machine-learning-for-web-developers/ml-3.png&quot; alt=&quot;Reinforcement learning diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This subset of machine learning is commonly used in games because it utilizes goal-oriented algorithms. Unlike supervised learning, each decision is NOT independent &#x2013; given a current input, the algorithm makes a decision and the &lt;em&gt;next input depends on this decision&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Just like I give my dog a pat on the head when he stops incessantly barking when the doorbell rings or put him in his kennel when he won&apos;t shut up, reinforcement algorithms are rewarded when making a goal-optimizing decision (e.g., scoring the max number of points) and penalized when making a poor one.&lt;/p&gt;
&lt;p&gt;The obvious applications for reinforcement learning algorithms include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;games like chess and Go (I highly recommend the AlphaGo documentary on Netflix, if you haven&apos;t seen it already.)&lt;/li&gt;
&lt;li&gt;robotics (teaching bots to complete desired tasks)&lt;/li&gt;
&lt;li&gt;autonomous vehicles&lt;/li&gt;
&lt;li&gt;robo-advisors that are trained to &lt;a href=&quot;https://analyticsindiamag.com/reinforcement-learning-goes-beyond-gaming-robotics-will-become-a-game-changer-in-2019/&quot;&gt;beat the stock market&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re interested in learning more about the algorithms behind reinforcement learning, Google search Q-learning, state-action-reward-state-action (SARSA), DQN, and the asynchronous advantage actor critic.&lt;/p&gt;
&lt;h2&gt;Remember to learn more&lt;/h2&gt;
&lt;p&gt;I hope that these examples have helped you gain a grasp on machine learning techniques and how each is used to influence the crazy world we live in today. While I sometimes find myself overwhelmed with all that there is to learn, starting somewhere is better than doing nothing, and remember that much of this is actually not very new at all &#x2013; we are just hearing about it more as data becomes more available and processing power cheaper.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Cross-cluster associations in Rails</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/cross-cluster-associations-rails/"/>
    <id>https://www.honeybadger.io/blog/cross-cluster-associations-rails/</id>
    <published>2023-01-30T00:00:00+00:00</published>
    <updated>2026-04-02T00:00:00+00:00</updated>
    <author>
      <name>Julie Kent</name>
    </author>
    <summary type="text">In this article, Julie discusses associations in Rails when the underlying data spans several databases. Learn how to connect a model across databases today.</summary>
    <content type="html">&lt;p&gt;One of the beauties of the Rails framework is the ability to utilize Ruby on Rails associations in your models. These associations allow you to access collections of records in your code with pleasant syntax, abstracting away the need to write underlying SQL queries. That abstraction holds as long as all your data lives in one place. The moment your &lt;a href=&quot;https://www.honeybadger.io/blog/pg-partition-manager/&quot;&gt;tables&lt;/a&gt; are spread across separate database clusters, certain association types stop working.&lt;/p&gt;
&lt;p&gt;This article walks through exactly where that boundary is and what Rails provides to work within it. We start with why the problem occurs and which associations in Rails are affected, and move into the database configuration and model hierarchy that supports multiple clusters and many to many relationships. From there we&apos;ll cover how different different data access patterns each interact with that decomposition.&lt;/p&gt;
&lt;p&gt;If you&apos;re looking for a Rails associations tutorial that covers multi-database setups specifically, this is it. We will also discuss many other things, so stick around.&lt;/p&gt;
&lt;h2&gt;Why databases end up in different clusters&lt;/h2&gt;
&lt;p&gt;When a Rails application stores all its data in a single database, Active Record associations are handled transparently, and you never think about the underlying SQL. The moment your data lives across multiple database clusters, that transparency breaks down. A &lt;code&gt;JOIN&lt;/code&gt; requires both tables to exist in the same database server. Attempting one across clusters produces an &lt;code&gt;ActiveRecord::StatementInvalid&lt;/code&gt; error like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ActiveRecord::StatementInvalid (Table &apos;people_cluster.humans&apos; doesn&apos;t exist)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isn&apos;t a configuration mistake. It&apos;s a hard physical constraint: database servers cannot &lt;code&gt;JOIN&lt;/code&gt; against tables they don&apos;t host. We have this problem in &lt;code&gt;has_many :through&lt;/code&gt; and &lt;code&gt;has_one :through&lt;/code&gt; associations, because those are the association types that generate intermediate &lt;code&gt;JOIN&lt;/code&gt; queries. Direct &lt;code&gt;has_many&lt;/code&gt; or &lt;code&gt;belongs_to&lt;/code&gt; relationships don&apos;t require a join  so they work across clusters without any modification.&lt;/p&gt;
&lt;p&gt;Understanding &lt;em&gt;when&lt;/em&gt; you&apos;ll hit this boundary is the first step. If a &lt;code&gt;User&lt;/code&gt; lives in the &lt;code&gt;accounts&lt;/code&gt; database and a &lt;code&gt;Post&lt;/code&gt; lives in the &lt;code&gt;content&lt;/code&gt; database, &lt;code&gt;User has_many :posts&lt;/code&gt; works fine. But if you add an intermediate &lt;code&gt;Subscription&lt;/code&gt; model in the &lt;code&gt;billing&lt;/code&gt; database and define &lt;code&gt;User has_many :posts, through: :subscriptions&lt;/code&gt;, Rails will attempt to join &lt;code&gt;subscriptions&lt;/code&gt; and &lt;code&gt;posts&lt;/code&gt; in a single query. That&apos;s where the cluster boundary becomes a problem.&lt;/p&gt;
&lt;h2&gt;The three-tier database configuration&lt;/h2&gt;
&lt;p&gt;Before writing any model code, the database configuration needs to reflect the multi-cluster layout. Rails uses a three-tier structure in &lt;code&gt;config/database.yml&lt;/code&gt; for this purpose. Each top-level environment key contains nested database names, and each of those contains the connection details for that cluster.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# config/database.yml
default: &amp;amp;default
  adapter: postgresql
  pool: &amp;lt;%= ENV.fetch(&amp;quot;RAILS_MAX_THREADS&amp;quot;) { 5 } %&amp;gt;

development:
  primary:
    &amp;lt;&amp;lt;: *default
    database: myapp_primary_dev

  accounts:
    &amp;lt;&amp;lt;: *default
    database: myapp_accounts_dev
    migrations_paths: db/accounts_migrate

  content:
    &amp;lt;&amp;lt;: *default
    database: myapp_content_dev
    migrations_paths: db/content_migrate

production:
  primary:
    &amp;lt;&amp;lt;: *default
    database: myapp_primary_prod
    username: &amp;lt;%= ENV[&apos;DB_USER&apos;] %&amp;gt;
    password: &amp;lt;%= ENV[&apos;DB_PASSWORD&apos;] %&amp;gt;

  accounts:
    &amp;lt;&amp;lt;: *default
    database: myapp_accounts_prod
    username: &amp;lt;%= ENV[&apos;DB_USER&apos;] %&amp;gt;
    password: &amp;lt;%= ENV[&apos;DB_PASSWORD&apos;] %&amp;gt;

  content:
    &amp;lt;&amp;lt;: *default
    database: myapp_content_prod
    username: &amp;lt;%= ENV[&apos;DB_USER&apos;] %&amp;gt;
    password: &amp;lt;%= ENV[&apos;DB_PASSWORD&apos;] %&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;migrations_paths&lt;/code&gt; key is non-optional if you want Rails generators and &lt;code&gt;db:migrate&lt;/code&gt; to route migrations to the correct directory. Without it, all migrations default to &lt;code&gt;db/migrate&lt;/code&gt; and get applied to the primary database. Each secondary database should also have a corresponding abstract record class that Rails models inherit from. Generators handle this automatically when you pass the &lt;code&gt;--database&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rails generate model Subscription plan:string --database accounts

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This produces an &lt;code&gt;AccountsRecord&lt;/code&gt; class if one doesn&apos;t already exist, and the generated &lt;code&gt;Subscription&lt;/code&gt; model inherits from it.&lt;/p&gt;
&lt;h2&gt;Abstract record classes and connection routing&lt;/h2&gt;
&lt;p&gt;The abstract record classes are the mechanism Rails uses to route queries to the correct cluster. Each one calls &lt;code&gt;connects_to&lt;/code&gt; to declare which database it maps to for writing and reading operations. Your application will typically have three layers in this hierarchy.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/application_record.rb
class ApplicationRecord &amp;lt; ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :primary, reading: :primary }
end

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/accounts_record.rb
class AccountsRecord &amp;lt; ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :accounts, reading: :accounts }
end

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/content_record.rb
class ContentRecord &amp;lt; ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :content, reading: :content }
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The User model is a good anchor for understanding this hierarchy.  It lives in the &lt;code&gt;accounts&lt;/code&gt; cluster and inherits from &lt;code&gt;AccountsRecord&lt;/code&gt;. Models in the &lt;code&gt;content&lt;/code&gt; cluster inherit from &lt;code&gt;ContentRecord&lt;/code&gt;. Everything else inherits from &lt;code&gt;ApplicationRecord&lt;/code&gt; and hits the primary database. This inheritance chain is how Active Record determines which connection pool to use when executing a query. It walks up the class hierarchy until it finds a class that has called &lt;code&gt;connects_to&lt;/code&gt;.
&lt;img src=&quot;https://hackmd.io/_uploads/HyMnBF1PWe.png&quot; alt=&quot;Rails multi-cluster database hierarchy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A common mistake is calling &lt;code&gt;establish_connection&lt;/code&gt; on individual models instead of using abstract classes. Each &lt;code&gt;establish_connection&lt;/code&gt; call opens a separate connection pool. If you have 50 models in the &lt;code&gt;accounts&lt;/code&gt; database, each calling &lt;code&gt;establish_connection&lt;/code&gt;, you end up with 50 connection pools pointing at the same server. Abstract classes solve this by sharing a single pool across all models that inherit from them.&lt;/p&gt;
&lt;h2&gt;How cross-cluster associations in Rails actually work&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;disable_joins: true&lt;/code&gt; option is the direct mechanism for making &lt;code&gt;through&lt;/code&gt; associations work when the involved tables live in different clusters. Rails &lt;code&gt;has_many&lt;/code&gt; is the most commonly used association type, and it&apos;s the one most directly affected by cluster boundaries. When Rails encounters this option on an association, it abandons the single &lt;code&gt;JOIN&lt;/code&gt; query strategy and instead issues two (or more) sequential &lt;code&gt;SELECT&lt;/code&gt; statements, piping the IDs from the first query into a &lt;code&gt;WHERE ... IN (...)&lt;/code&gt; clause in the second.&lt;/p&gt;
&lt;p&gt;Here&apos;s a concrete model setup spanning three clusters. The model setup below is a many-to-many relationship, a User connects to Posts through Subscriptions, and it&apos;s the pattern that most directly exposes the cross-cluster problem.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/user.rb - lives in the accounts database
class User &amp;lt; AccountsRecord
  has_many :subscriptions
  has_many :posts, through: :subscriptions, disable_joins: true
end

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/subscription.rb - lives in the accounts database
class Subscription &amp;lt; AccountsRecord
  belongs_to :user
  has_many :posts
end

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/post.rb - lives in the content database
class Post &amp;lt; ContentRecord
  belongs_to :subscription
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you call &lt;code&gt;user.posts&lt;/code&gt;, Rails generates this pair of queries instead of a single &lt;code&gt;JOIN&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Query 1: fetch subscription IDs from the accounts cluster
SELECT &amp;quot;subscriptions&amp;quot;.&amp;quot;id&amp;quot;
FROM &amp;quot;subscriptions&amp;quot;
WHERE &amp;quot;subscriptions&amp;quot;.&amp;quot;user_id&amp;quot; = 1

-- Query 2: fetch posts from the content cluster using those IDs
SELECT &amp;quot;posts&amp;quot;.*
FROM &amp;quot;posts&amp;quot;
WHERE &amp;quot;posts&amp;quot;.&amp;quot;subscription_id&amp;quot; IN (4, 7, 12)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first query runs against the &lt;code&gt;accounts&lt;/code&gt; database to collect a primary key. The second runs against &lt;code&gt;content&lt;/code&gt;. Rails resolves the relationship by following the foreign keys, &lt;code&gt;user_id&lt;/code&gt; on subscriptions and &lt;code&gt;subscription_id&lt;/code&gt; on posts, across the two clusters. The first query collects the primary key values from subscriptions, then passes them into the &lt;code&gt;IN&lt;/code&gt; clause of the second query. Neither query attempts a cross-cluster join. Rails assembles the final result set in application memory.
&lt;img src=&quot;https://hackmd.io/_uploads/rJzart1Pbg.png&quot; alt=&quot;associations in rails - two sequential queries across clusters&quot; /&gt;
&lt;img src=&quot;https://hackmd.io/_uploads/HJpnHtkwWg.png&quot; alt=&quot;the failing single JOIN&quot; /&gt;
The same option works identically on &lt;code&gt;has_one :through&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/user.rb
class User &amp;lt; AccountsRecord
  has_one :profile
  has_one :avatar, through: :profile, disable_joins: true
end

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/profile.rb - accounts database
class Profile &amp;lt; AccountsRecord
  belongs_to :user
  has_one :avatar
end

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/avatar.rb - content database
class Avatar &amp;lt; ContentRecord
  belongs_to :profile
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;user.avatar&lt;/code&gt; will execute two queries: one to get the &lt;code&gt;profile_id&lt;/code&gt;, another to fetch the avatar record from the content cluster.&lt;/p&gt;
&lt;h2&gt;When &lt;code&gt;disable_joins&lt;/code&gt; must be set explicitly&lt;/h2&gt;
&lt;p&gt;Rails does not automatically detect cluster boundaries and insert &lt;code&gt;disable_joins&lt;/code&gt; for you. Association loading in Active Record is lazy. The SQL for an association is determined at the point the association is defined on the model, not when it&apos;s actually triggered. By the time &lt;code&gt;user.posts&lt;/code&gt; executes, Rails has already decided whether to use a &lt;code&gt;JOIN&lt;/code&gt; or separate queries based on the association declaration.&lt;/p&gt;
&lt;p&gt;This means every &lt;code&gt;through&lt;/code&gt; association that crosses a cluster boundary needs &lt;code&gt;disable_joins: true&lt;/code&gt; on the declaration.&lt;/p&gt;
&lt;p&gt;A practical way to audit your models is to look for any &lt;code&gt;through:&lt;/code&gt; association where the source model and the target model inherit from different abstract record classes. If &lt;code&gt;User &amp;lt; AccountsRecord&lt;/code&gt; and &lt;code&gt;Post &amp;lt; ContentRecord&lt;/code&gt;, then &lt;code&gt;has_many :posts, through: :subscriptions&lt;/code&gt; needs &lt;code&gt;disable_joins: true&lt;/code&gt; regardless of where &lt;code&gt;Subscription&lt;/code&gt; lives.&lt;/p&gt;
&lt;h2&gt;Eager loading across clusters&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;disable_joins&lt;/code&gt; option affects how associations are loaded, but it does not change how eager loading strategies interact with cross-cluster data. Understanding this distinction matters for avoiding N+1 queries in multi-database setups.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;eager_load&lt;/code&gt; is off the table for cross-cluster associations. It generates a &lt;code&gt;LEFT OUTER JOIN&lt;/code&gt;, which has the same physical limitation as a regular &lt;code&gt;JOIN&lt;/code&gt; , both tables must be on the same server. If you attempt &lt;code&gt;User.eager_load(:posts)&lt;/code&gt; where posts live in a different cluster, you will get the same &lt;code&gt;StatementInvalid&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;preload&lt;/code&gt; is the correct strategy. It issues separate queries for each association and assembles the relationship in Ruby. This is structurally identical to what &lt;code&gt;disable_joins&lt;/code&gt; does for a single record. The difference is scale: &lt;code&gt;preload&lt;/code&gt; batches the second query across all loaded parent records.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# This works across clusters.
# Query 1: SELECT &amp;quot;users&amp;quot;.* FROM &amp;quot;users&amp;quot;
# Query 2: SELECT &amp;quot;posts&amp;quot;.* FROM &amp;quot;posts&amp;quot; WHERE &amp;quot;posts&amp;quot;.&amp;quot;subscription_id&amp;quot; IN (...)
users = User.preload(:posts).all

users.each do |user|
  user.posts.each { |post| puts post.title }  # No additional queries fired
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;includes&lt;/code&gt; will work in cases where it delegates to &lt;code&gt;preload&lt;/code&gt; internally, which it does by default when there are no conditions referencing the associated table. If you add a &lt;code&gt;.where&lt;/code&gt; clause that touches the associated table&apos;s columns, &lt;code&gt;includes&lt;/code&gt; switches to &lt;code&gt;eager_load&lt;/code&gt; behavior, and will fail across clusters. When in doubt about which strategy &lt;code&gt;includes&lt;/code&gt; will choose, be explicit and use &lt;code&gt;preload&lt;/code&gt; directly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# includes delegates to preload here, works across clusters
User.includes(:posts).all

# includes switches to eager_load because of the where clause, fails across clusters
User.includes(:posts).where(&amp;quot;posts.published = ?&amp;quot;, true)

# Use preload + a separate where for cross-cluster filtering
User.preload(:posts).all.select { |u| u.posts.any?(&amp;amp;:published?) }
# Or filter in application code after loading

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Scoped associations and cross-cluster filtering&lt;/h2&gt;
&lt;p&gt;One of the more subtle interactions in multi-database setups is scoped associations. When you define a scope on a &lt;code&gt;has_many&lt;/code&gt; that crosses clusters, the scope&apos;s SQL runs against the target database, not the source.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class User &amp;lt; AccountsRecord
  has_many :subscriptions
  has_many :published_posts,
           -&amp;gt; { where(published: true) },
           through: :subscriptions,
           source: :posts,
           class_name: &amp;quot;Post&amp;quot;,
           disable_joins: true
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;where(published: true)&lt;/code&gt; clause gets appended to the second query, the one that runs against the &lt;code&gt;content&lt;/code&gt; database. This is correct behavior, and it means your scopes can reference columns on the target table without issue. What you cannot do is reference columns from the intermediate table in that scope, because the intermediate query has already completed by the time the scoped query executes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# This will fail because subscriptions.active is not a column in the content database
has_many :active_posts,
         -&amp;gt; { where(&amp;quot;subscriptions.active = ?&amp;quot;, true) },
         through: :subscriptions,
         source: :posts,
         disable_joins: true

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Filter intermediate records by adding a scope to the intermediate association instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class User &amp;lt; AccountsRecord
  has_many :active_subscriptions, -&amp;gt; { where(active: true) }, class_name: &amp;quot;Subscription&amp;quot;
  has_many :active_posts, through: :active_subscriptions, source: :posts, disable_joins: true
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the filtering on &lt;code&gt;subscriptions.active&lt;/code&gt; happens in the first query, against the &lt;code&gt;accounts&lt;/code&gt; database, and only the IDs from active subscriptions get passed to the second query.&lt;/p&gt;
&lt;h2&gt;Horizontal sharding and cross-shard associations&lt;/h2&gt;
&lt;p&gt;Splitting one logical database across multiple servers based on a partition key like &lt;code&gt;tenant_id&lt;/code&gt;  introduces a second dimension to the cross-cluster problem. The &lt;code&gt;disable_joins&lt;/code&gt; mechanism still applies, but the connection routing becomes more involved.&lt;/p&gt;
&lt;p&gt;Rails provides &lt;code&gt;connected_to&lt;/code&gt; for switching between shards within a request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;ActiveRecord::Base.connected_to(role: :writing, shard: :shard_one) do
  User.find(1)  # Hits shard_one
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When associations span both clusters and shards, you need to ensure both the shard context and the &lt;code&gt;disable_joins&lt;/code&gt; option are in place. A &lt;code&gt;User&lt;/code&gt; on &lt;code&gt;shard_one&lt;/code&gt; accessing posts that live in a separate &lt;code&gt;content&lt;/code&gt; database still needs the same two-query decomposition.&lt;/p&gt;
&lt;p&gt;Rails 8 added introspection methods that make reasoning about shard topology easier at runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class ShardedBase &amp;lt; ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: {
    shard_one: { writing: :shard_one },
    shard_two: { writing: :shard_two }
  }
end

class User &amp;lt; ShardedBase; end

User.shard_keys         # =&amp;gt; [:shard_one, :shard_two]
User.sharded?           # =&amp;gt; true

ShardedBase.connected_to_all_shards do
  User.current_shard    # Yields :shard_one, then :shard_two
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;connected_to_all_shards&lt;/code&gt; is particularly useful for background jobs that need to process records across every shard. It iterates over each shard in sequence, switching the connection context for each block execution.&lt;/p&gt;
&lt;p&gt;For tenant-based sharding, the &lt;code&gt;lock: true&lt;/code&gt; default on shard switching prevents accidental tenant hopping mid-request. This is a safety mechanism: once a request is routed to a tenant&apos;s shard, the application code cannot switch to a different tenant&apos;s shard without explicitly passing &lt;code&gt;lock: false&lt;/code&gt;. Cross-cluster associations within a single tenant&apos;s shard still use &lt;code&gt;disable_joins&lt;/code&gt; for associations that touch a different database cluster.&lt;/p&gt;
&lt;h2&gt;Testing cross-cluster associations&lt;/h2&gt;
&lt;p&gt;Testing multi-database setups requires that your test environment mirrors the production database topology. Rails&apos; test framework supports this, but the configuration must be explicit.&lt;/p&gt;
&lt;p&gt;Each database in &lt;code&gt;database.yml&lt;/code&gt; needs a &lt;code&gt;test&lt;/code&gt; environment block. Fixtures and factory-based test data must target the correct database. If a &lt;code&gt;User&lt;/code&gt; factory creates a record in the &lt;code&gt;accounts&lt;/code&gt; database and a &lt;code&gt;Post&lt;/code&gt; factory creates one in &lt;code&gt;content&lt;/code&gt;, the association between them only works if both records exist in their respective databases within the same test transaction.&lt;/p&gt;
&lt;p&gt;Rails wraps each test in a transaction by default, but that transaction is per-connection. With multiple databases, each connection gets its own transaction. This means test cleanup (the automatic rollback at the end of each test) happens independently on each database. If your test writes a &lt;code&gt;User&lt;/code&gt; to &lt;code&gt;accounts&lt;/code&gt; and a &lt;code&gt;Post&lt;/code&gt; to &lt;code&gt;content&lt;/code&gt;, both will be rolled back, but only if the test framework knows about both connections.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;fixtures&lt;/code&gt; declaration handles this automatically when models inherit from the correct abstract class. For factory-based setups (FactoryBot, Fabricator), ensure each factory&apos;s &lt;code&gt;create&lt;/code&gt; strategy hits the right database by letting the model&apos;s own &lt;code&gt;connects_to&lt;/code&gt; routing do the work.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # User inherits from AccountsRecord and writes to accounts DB automatically
    name { Faker::Name.name }
  end
end

# spec/factories/posts.rb
FactoryBot.define do
  factory :post do
    # Post inherits from ContentRecord and writes to content DB automatically
    association :subscription
    title { Faker::Lorem.sentence }
  end
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To verify that cross-cluster associations are firing the expected number of queries, subscribe to the &lt;code&gt;sql.active_record&lt;/code&gt; notification:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# spec/support/query_counter.rb
module QueryCounter
  def assert_query_count(expected, &amp;amp;block)
    count = 0
    callback = -&amp;gt;(_name, _start, _finish, _id, payload) do
      count += 1 unless payload[:name] == &amp;quot;SCHEMA&amp;quot; || payload[:sql].start_with?(&amp;quot;EXPLAIN&amp;quot;)
    end

    ActiveSupport::Notifications.subscribed(callback, &amp;quot;sql.active_record&amp;quot;, &amp;amp;block)
    assert_equal expected, count, &amp;quot;Expected #{expected} queries, got #{count}&amp;quot;
  end
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;has_many :through&lt;/code&gt; with &lt;code&gt;disable_joins: true&lt;/code&gt; on a single record should produce exactly 2 queries. If you see 1, the join is still being attempted (and will fail in production against separate servers). If you see N+1, eager loading isn&apos;t working as expected.&lt;/p&gt;
&lt;h2&gt;Some caveats&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;disable_joins&lt;/code&gt; solves the association loading problem, but it does not extend to query chaining. You cannot chain &lt;code&gt;.where&lt;/code&gt;, &lt;code&gt;.order&lt;/code&gt;, or &lt;code&gt;.group&lt;/code&gt; clauses that reference columns across clusters on a single Active Record relation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# This does not work, you cannot filter products by order columns across clusters
customer.purchased_products.where(&amp;quot;orders.total &amp;gt; ?&amp;quot;, 100)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For queries that need to filter or sort based on data in multiple clusters, decompose them manually. Fetch the IDs or values you need from one cluster, then use them as inputs to a query against the other:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;high_value_order_ids = Order.where(customer_id: customer.id)
                            .where(&amp;quot;total &amp;gt; ?&amp;quot;, 100)
                            .pluck(:id)

line_item_product_ids = LineItem.where(order_id: high_value_order_ids).pluck(:product_id)

products = Product.where(id: line_item_product_ids)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the same decomposition that &lt;code&gt;disable_joins&lt;/code&gt; performs internally, but done explicitly so you can apply filtering at each stage. It&apos;s more verbose, but it makes the cluster boundaries visible in the code rather than hiding them behind associations in Rails syntax.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Heroku vs Vercel: Choosing the right platform for your app</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/heroku-vs-vercel/"/>
    <id>https://www.honeybadger.io/blog/heroku-vs-vercel/</id>
    <published>2026-03-24T00:00:00+00:00</published>
    <updated>2026-03-24T00:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">Heroku and Vercel both simplify application deployment, but they take very different approaches. This article breaks down their architectures, workflows, costs, and use cases to help you decide which platform fits your team and application best.</summary>
    <content type="html">&lt;p&gt;When it comes to deploying web applications, developers face issues picking from the huge list of cloud platform options. Two popular choices that frequently come up in discussions are Heroku and Vercel, each offering distinct approaches to application hosting and deployment. Understanding the differences between these platforms is crucial for making an informed decision that aligns with your project&apos;s needs, budget, and technical requirements.&lt;/p&gt;
&lt;p&gt;This Heroku vs Vercel comparison examines the factors that impact your choice: architectural paradigms, deployment workflows, primary use cases, cost structures, and security models.&lt;/p&gt;
&lt;h2&gt;Heroku vs Vercel: Things to consider&lt;/h2&gt;
&lt;p&gt;Before diving into the specifics, it&apos;s important to understand the fundamental positioning of these platforms. Heroku, acquired by Salesforce in 2010, was one of the original Platform as a Service (PaaS) providers, designed to simplify application deployment across a wide range of technologies and frameworks. Vercel, on the other hand, emerged from the modern frontend ecosystem, initially created by the team behind Next.js to provide an optimal deployment experience for JavaScript and TypeScript applications.&lt;/p&gt;
&lt;p&gt;When evaluating these platforms, consider your application&apos;s architecture, your team&apos;s expertise, scalability requirements, and budget constraints. Heroku excels at supporting traditional server-based applications and offers extensive support for multiple programming languages, including Ruby, Python, Java, PHP, Node.js, and Go. Vercel has carved out a strong niche in the frontend and full-stack app ecosystem, with exceptional support for Next.js, React, Vue, and other frontend frameworks.&lt;/p&gt;
&lt;p&gt;The choice between them often comes down to whether you&apos;re building a traditional backend-heavy application with potentially complex server requirements, or a modern frontend-first application with serverless API routes. Your team&apos;s familiarity with the deployment paradigm also matters. Heroku follows a more traditional server-based model, while Vercel embraces &lt;a href=&quot;https://www.honeybadger.io/blog/serverless-computing-tutorial/&quot;&gt;serverless computing&lt;/a&gt; and edge computing principles.&lt;/p&gt;
&lt;h2&gt;Selling point&lt;/h2&gt;
&lt;p&gt;Heroku&#x2019;s core selling point is operational simplicity for backend-heavy applications. It abstracts servers, scaling, and infrastructure into a mature PaaS with first-class support for long-running processes, background workers, scheduled jobs, and managed add-ons (Postgres, Redis, etc). If you are building traditional web apps, APIs, or worker-based systems, Heroku lets you focus on application logic and backend logic instead of platform mechanics.&lt;/p&gt;
&lt;p&gt;Vercel&apos;s main selling point is frontend velocity and deployment confidence. It is tightly integrated with modern frameworks (especially Next.js) and offers instant global deployments, edge rendering, and preview deployments for every pull request (even though Heroku has a similar feature, review apps). Vercel is optimized for frontend apps and provides serverless infrastructure that scales automatically. For engineers prioritizing frontend performance, DX, and rapid feedback loops, look no further than Vercel.&lt;/p&gt;
&lt;h2&gt;Architectural differences between Vercel vs Heroku&lt;/h2&gt;
&lt;p&gt;The architectural philosophies of Heroku and Vercel diverge significantly, which shows their different strengths and target audiences.&lt;/p&gt;
&lt;p&gt;Heroku operates on a dyno-based architecture. Dynos are lightweight Linux containers that run your application code. When you deploy to Heroku, your application runs on one or more dynos, which can be scaled horizontally by adding more dynos or vertically by upgrading to more powerful dyno types. This model is conceptually similar to running your application on traditional servers, but with the abstraction and convenience of a managed platform. Heroku also provides a robust add-on ecosystem, allowing you to attach databases, caching layers, monitoring tools, and other services to your application with minimal configuration.&lt;/p&gt;
&lt;p&gt;Vercel, in contrast, embraces a serverless architecture with a strong emphasis on edge computing. When you deploy a frontend application to Vercel, your static assets are distributed across a global Content Delivery Network (CDN) for fast load times, regardless of user location. For dynamic functionality, Vercel uses serverless functions that execute on demand without requiring you to manage any server infrastructure. This serverless computing model is particularly efficient for full stack apps that combine static websites with API endpoints.&lt;/p&gt;
&lt;h2&gt;Deployment workflows&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/heroku-vs-vercel/deployment-workflow.png&quot; alt=&quot;heroku vs vercel deployment workflow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The deployment experience differs substantially between these platforms, though both aim to make deployment as straightforward as possible.&lt;/p&gt;
&lt;p&gt;Heroku pioneered the Git-based deployment workflow. To deploy to Heroku, you typically connect your Git repository to Heroku and push your code using &lt;code&gt;git push heroku main&lt;/code&gt;. Heroku automatically detects your application&apos;s language using buildpacks, installs dependencies, and starts your application. This workflow is intuitive for developers already familiar with Git and provides a clear mental model of deployment as a simple code push. Heroku also supports continuous deployment from GitHub repositories, automatically deploying when changes are pushed to specific branches.&lt;/p&gt;
&lt;p&gt;The Heroku platform provides a Procfile mechanism for defining how your application should run.&lt;/p&gt;
&lt;p&gt;Vercel takes deployment automation even further, with particularly seamless integration for frameworks it supports natively. When you connect a Git repository to Vercel, every push triggers an automatic deployment with a unique preview URL. This makes it incredibly easy to review changes before they go to production. Merging to your main branch automatically deploys to your production environment. Vercel&apos;s build system automatically detects your framework and applies appropriate build configurations, often requiring zero configuration for popular frontend frameworks like Next.js, Create React App, or Nuxt.js.&lt;/p&gt;
&lt;p&gt;One of Vercel&apos;s standout features is its preview deployments. Every pull request gets its own deployment URL, complete with production-like infrastructure, making code reviews more effective and allowing stakeholders to interact with changes before they&apos;re merged. Vercel also makes it easy to attach custom domains to both production and preview deployments with automatic SSL.&lt;/p&gt;
&lt;h2&gt;Heroku vs Vercel: Their primary use cases&lt;/h2&gt;
&lt;p&gt;Understanding where each platform excels helps clarify which might be the better choice for your specific needs.&lt;/p&gt;
&lt;p&gt;Heroku shines for traditional web applications, particularly those built with backend frameworks like Ruby on Rails, Django, Express.js, or Spring Boot. If you&apos;re building a monolithic application, an API server with complex business logic, or an application requiring persistent connections like real-time chat servers, Heroku provides the infrastructure and flexibility you need. The platform is also well-suited for applications requiring background job processing, scheduled tasks, or applications that need to integrate with specific databases or services through Heroku&apos;s extensive add-on marketplace.&lt;/p&gt;
&lt;p&gt;Development teams working with polyglot architectures appreciate Heroku&apos;s multi-language support. This multi-language support makes Heroku attractive for teams running complex backends across different stacks. You can run different components of your application using different technologies, all within the same platform ecosystem. The add-on ecosystem is particularly valuable for teams that want managed services for databases, caching, monitoring, logging, and other infrastructure concerns without managing these services independently.&lt;/p&gt;
&lt;p&gt;Vercel is the natural choice for modern frontend applications and full-stack JavaScript applications, particularly those built with Next.js. If you&apos;re building a static site, a Jamstack application, a React-based dashboard, or a marketing website with dynamic elements, Vercel&apos;s architecture delivers exceptional performance and developer experience. The platform&apos;s serverless functions are well-suited for API routes that support frontend apps, particularly for use cases like form submissions, authentication endpoints, or data transformations.&lt;/p&gt;
&lt;p&gt;The combination of edge caching, automatic image optimization, and serverless functions makes Vercel particularly effective for content-heavy websites, e-commerce storefronts, and applications where global performance matters.&lt;/p&gt;
&lt;h2&gt;Security features in Heroku and Vercel&lt;/h2&gt;
&lt;p&gt;Security is a core concern for both platforms, but their approaches reflect different architectures. Heroku focuses on platform and workload isolation: applications run in isolated containers, HTTPS is supported via managed SSL certificates, and the platform includes network-level protections like DDoS mitigation (minimum protection). Heroku also emphasizes compliance and enterprise controls, with certifications such as SOC 2, ISO 27001, and PCI DSS.
These guarantees extend to managed databases like Heroku Postgres, which inherit platform-level security controls by default.&lt;/p&gt;
&lt;p&gt;Vercel, by contrast, follows a secure-by-default, serverless model. All deployments are automatically served over HTTPS, protected by a global edge network with built-in DDoS protection. Secrets are encrypted and scoped per environment (production, preview, development), preview deployments can be access-restricted, and enterprise plans add SSO, IP allowlisting, and deployment protection. Vercel maintains SOC 2 Type II compliance and provides fine-grained controls suited to frontend-centric and serverless applications.&lt;/p&gt;
&lt;h2&gt;Cost considerations&lt;/h2&gt;
&lt;p&gt;Understanding the cost structure of each platform is relevant for budget planning. Let&apos;s see the costs using an example of a full-stack application. Historically, Heroku offered free dynos, but today Vercel&#x2019;s free tier is the more common entry point for developers experimenting or running small projects. So, for the sake of this example, we will be ignoring the free tier side of things because only Vercel offers some form of free tier.&lt;/p&gt;
&lt;p&gt;Consider a full-stack application with a Next.js frontend with API routes, a PostgreSQL database, and Redis for caching.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hosting&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heroku:&lt;/strong&gt; $25 (1&#xd7; Standard-1X dyno, 512&#x202f;MB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; $20 (1&#xd7; Pro seat)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Postgres DB&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heroku:&lt;/strong&gt; $50 (Standard-0, 64&#x202f;GB storage)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; &#x2248;$7 (20&#x202f;GB at $0.35/GB) + compute (~$0&#x2013;50)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Redis cache&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heroku:&lt;/strong&gt; $60 (Premium-2, 250&#x202f;MB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; $0&#x2013;$10 (Upstash free 256&#x202f;MB tier or $10 plan)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bandwidth&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heroku:&lt;/strong&gt; ~Free (up to 2&#x202f;TB soft limit)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; 1&#x202f;TB included (then $0.15/GB)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Total (est.)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heroku:&lt;/strong&gt; &#x2248;$135&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; &#x2248;$30&#x2013;40&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Heroku total (~$135) assumes one dyno ($25), a Standard-0 Postgres ($50), and Redis Premium-2 ($60). Vercel&#x2019;s total (~$30&#x2013;40) is roughly $20 for Pro plus minimal DB/cache: Neon storage ($7) and possibly an Upstash plan ($5&#x2013;10). Actual Neon compute usage (serverless scaling) could raise Vercel&#x2019;s cost if the database is very active; likewise, as the app grows, heavy bandwidth or function invocation beyond free allowances could add fees.&lt;/p&gt;
&lt;p&gt;If you take advantage of the free plan provided by Vercel, you may notice significant savings.&lt;/p&gt;
&lt;p&gt;While the estimated Vercel total appears lower, it is important to understand how Vercel pricing scales. Unlike Heroku&#x2019;s largely fixed, tier-based pricing, Vercel follows a usage-based model. The Pro plan includes a base monthly fee, but many resources are metered and billed linearly as usage increases.&lt;/p&gt;
&lt;h2&gt;Choosing the right platform for your team&lt;/h2&gt;
&lt;p&gt;Selecting between Heroku vs Vercel isn&apos;t about finding the objectively &amp;quot;better&amp;quot; platform; it&apos;s about identifying which one aligns with your specific needs. Heroku excels for traditional backend applications, supports multiple programming languages, has background jobs, and teams want an all-in-one cloud platform with a mature ecosystem. Vercel shines for modern frontend apps, static sites, Next.js projects, and teams prioritizing global performance and the seamless developer experience of automatic preview deployments.&lt;/p&gt;
&lt;p&gt;The most pragmatic approach is to evaluate both platforms against your actual requirements. Consider your technology stack, team expertise, scalability needs, and budget. Many teams even pilot projects on each platform to experience the workflows firsthand. Remember that your choice may not be permanent. Some people reconsider their platform choice to reduce vendor lock-in, and both Heroku and Vercel make it possible to migrate. Many successful organizations often start with one platform and evolve their strategy as needs change. The platform that reduces friction, supports your stack, and enables you to deliver value quickly is the right choice for your team.&lt;/p&gt;
&lt;p&gt;Whichever platform you choose, you&apos;ll want visibility into what&apos;s actually happening in production. Honeybadger works with both&#x2014;full-stack monitoring that takes minutes to set up. &lt;a href=&quot;https://app.honeybadger.io/users/sign_up&quot;&gt;Try it free for 30 days.&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building a Django Chat App with WebSockets</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/django-channels-websockets-chat/"/>
    <id>https://www.honeybadger.io/blog/django-channels-websockets-chat/</id>
    <published>2022-09-22T00:00:00+00:00</published>
    <updated>2026-03-13T00:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">Building stateful web applications can be tricky, unless you use a framework, of course. Django to the rescue! In this article, learn how to build a Djano chat app using Django Channels and WebSockets.</summary>
    <content type="html">&lt;p&gt;Django is well known for being used to develop servers for HTTP connections and requests for applications. Unfortunately, when building Django chat app or any chat app that requires the connection to remain open for a two-way connection, using an HTTP connection is inefficient.&lt;/p&gt;
&lt;p&gt;WebSockets provide a means of opening a two-way connection between the client and the server so that all users connected to the open network can get related data in real time. It is a stateful protocol, which means connection authentication is only required once; the client credential is stored, and there is no further need for authentication until the connection is lost.&lt;/p&gt;
&lt;p&gt;In this article, I&#x2019;ll briefly introduce you to WebSocket and its usefulness. Then, I&#x2019;ll show you how to use it in Django using Django channels and create WebSocket connections with JavaScript to connect with the Django server.&lt;/p&gt;
&lt;p&gt;We will build a simple chatbox to make things more realistic. You can find the code on &lt;a href=&quot;https://github.com/khabdrick/django-channels-tutorial&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Basic understanding of Django.&lt;/li&gt;
&lt;li&gt;Basic understanding of JavaScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Characteristics of WebSockets&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;WebSockets is a bidirectional protocol. Therefore, the client and server can exchange data simultaneously without delays or intervention. WebSockets is considered full-duplex communication for the same reason.&lt;/li&gt;
&lt;li&gt;WebSockets is a stateful protocol. Therefore, after initial connection authentication, the client credential is saved, and further authentication is not required until the connection is lost.&lt;/li&gt;
&lt;li&gt;WebSockets don&apos;t need any special browsers to function; it works on all browsers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to use WebSockets&lt;/h2&gt;
&lt;p&gt;WebSockets are used when you want to build any kind of real-time chat application, ranging from complex applications, such as multiplayer games played on the internet, to less complex ones, such as chat applications.&lt;/p&gt;
&lt;p&gt;An alternative method of building a chat app without using WebSockets is to use JavaScript to query the database after a few seconds to get current data from the chatbox. For example, you could add a database connection to store messages in a chat log. As you can imagine, this is not scalable because if there are thousands of users, the number of requests it could generate might cause the server to crash. Additionally, this method will not work when you want to build something like a video call application.&lt;/p&gt;
&lt;h2&gt;How to use Django WebSockets&lt;/h2&gt;
&lt;p&gt;Using WebSockets in Django utilizes asynchronous Python and Django channels, making the process straightforward. Using Django channels, you can create an ASGI server, and then create a group where users can send text messages to all the other users in the group in real time. This way, you are not communicating with a particular user, but with a group, multiple users can be added.&lt;/p&gt;
&lt;p&gt;If you are chatting with your friend on Twitter, you and your friend are in one group, represented by the chatbox.&lt;/p&gt;
&lt;h2&gt;Configure Django to use ASGI&lt;/h2&gt;
&lt;p&gt;If you don&#x2019;t already have a Django project, create a folder where you want to store the code for your project, &lt;code&gt;cd&lt;/code&gt; into it, and run &lt;code&gt;startproject&lt;/code&gt; to create a new Django project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;django-admin startproject project .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, create a new Django chat app by running &lt;code&gt;$ python3 manage.py startapp app&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You need to inform your Django project that a new app has been added. To do this, update the &lt;code&gt;project/settings.py&lt;/code&gt; file and add &lt;code&gt;&apos;app&apos;&lt;/code&gt; to the &lt;strong&gt;&lt;code&gt;INSTALLED_APPS&lt;/code&gt;&lt;/strong&gt; list. It&#x2019;ll look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# project/settings.py
INSTALLED_APPS = [
   ...
   &apos;chat&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now install &lt;a href=&quot;https://channels.readthedocs.io/en/stable/&quot;&gt;Django channels&lt;/a&gt; by running the following command on your command line.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install channels
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;code&gt;project/settings.py&lt;/code&gt; file and add &lt;code&gt;&apos;channels&apos;&lt;/code&gt; to the &lt;code&gt;INSTALLED_APPS&lt;/code&gt; list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# project/settings.py
INSTALLED_APPS = [
   ...
   &apos;channels&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While you are in the &lt;code&gt;settings.py&lt;/code&gt; file, you need to set a configuration to enable the Django channel and Django to communicate with each other using a message broker. We can use a tool like Redis for this, but for this tutorial, we will use the local backend. Paste the following code into your &lt;code&gt;settings.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ASGI_APPLICATION = &amp;quot;project.routing.application&amp;quot; #routing.py will be created later
CHANNEL_LAYERS = {
    &apos;default&apos;: {
        &apos;BACKEND&apos;: &amp;quot;channels.layers.InMemoryChannelLayer&amp;quot;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the code above, &lt;code&gt;ASGI_APPLICATION&lt;/code&gt; is needed to run the ASGI server and tell Django what to do when an event happens. This configuration will be placed in a file named &lt;code&gt;routing.py&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Build a minimalistic Django chat app&lt;/h2&gt;
&lt;p&gt;Next, we will create a chat app chatbox that authenticated users can access via a URL and chat with each other. To get this going, open your &lt;code&gt;app/views.py&lt;/code&gt; file and paste the code below to pass the chatbox name from the URL to the HTML file (&lt;code&gt;chatbox.html&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.shortcuts import render

def chat_box(request, chat_box_name):
    # we will get the chatbox name from the url
    return render(request, &amp;quot;chatbox.html&amp;quot;, {&amp;quot;chat_box_name&amp;quot;: chat_box_name})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, replace the code you have in &lt;code&gt;project/urls.py&lt;/code&gt; with the following code. This will handle the chatbox name stated in the browser (&lt;code&gt;http://127.0.0.1:8002/chat/**chatboxname**/&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from django.urls import path
from app.views import chat_box

urlpatterns = [
    path(&amp;quot;admin/&amp;quot;, admin.site.urls),
    path(&amp;quot;chat/&amp;lt;str:chat_box_name&amp;gt;/&amp;quot;, chat_box, name=&amp;quot;chat&amp;quot;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, let&#x2019;s start working on the &lt;a href=&quot;https://channels.readthedocs.io/en/stable/topics/consumers.html&quot;&gt;consumers&lt;/a&gt;. Consumers in channels help structure your code as a series of functions to be called whenever an event happens. Consumers are usually written in asynchronous Python. To start, create a new file in &lt;code&gt;app/&lt;/code&gt; folder named &lt;code&gt;consumers.py&lt;/code&gt; and paste the code shown below. What the following code is doing is handling what happens when the server connects, disconnects, receives a request, or sends a text.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatRoomConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.chat_box_name = self.scope[&amp;quot;url_route&amp;quot;][&amp;quot;kwargs&amp;quot;][&amp;quot;chat_box_name&amp;quot;]
        self.group_name = &amp;quot;chat_%s&amp;quot; % self.chat_box_name

        await self.channel_layer.group_add(self.group_name, self.channel_name)

        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.group_name, self.channel_name)
    # This function receive messages from WebSocket.
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json[&amp;quot;message&amp;quot;]
        username = text_data_json[&amp;quot;username&amp;quot;]

        await self.channel_layer.group_send(
            self.group_name,
            {
                &amp;quot;type&amp;quot;: &amp;quot;chatbox_message&amp;quot;,
                &amp;quot;message&amp;quot;: message,
                &amp;quot;username&amp;quot;: username,
            },
        )
    # Receive message from chat room group.
    async def chatbox_message(self, event):
        message = event[&amp;quot;message&amp;quot;]
        username = event[&amp;quot;username&amp;quot;]
        #send message and username of sender to websocket
        await self.send(
            text_data=json.dumps(
                {
                    &amp;quot;message&amp;quot;: message,
                    &amp;quot;username&amp;quot;: username,
                }
            )
        )

    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This Python code defines a Django Channels consumer that manages Django WebSockets connections for a real-time chat application. The &lt;code&gt;ChatRoomConsumer&lt;/code&gt; class inherits from &lt;code&gt;AsyncWebsocketConsumer&lt;/code&gt; and handles the complete lifecycle of a Django WebSocket connection. When a user connects, the &lt;code&gt;connect&lt;/code&gt; method extracts the chatbox name from the URL parameters, creates a unique group name for that chat room, adds the user&apos;s channel to that group (allowing multiple users to join the same chat room), and accepts the WebSocket connection. When a user disconnects, the &lt;code&gt;disconnect&lt;/code&gt; method cleanly removes their channel from the group to prevent memory leaks and ensure they no longer receive messages.&lt;/p&gt;
&lt;p&gt;The message handling happens through two coordinated methods that enable broadcasting to all users in a chat room. The &lt;code&gt;receive&lt;/code&gt; method is triggered whenever a message arrives from a client&apos;s WebSocket. It parses the JSON data to extract the message text and username, then uses the channel layer to broadcast this data to everyone in the group. The &lt;code&gt;chatbox_message&lt;/code&gt; method receives these broadcast messages and forwards them back to the individual WebSocket connections, ensuring that when one user sends a message, all other users in the same chat room receive it instantly. This pattern creates a publish-subscribe system where messages are distributed to all active participants in real-time.
Now, let&#x2019;s add the code for &lt;code&gt;routing.py&lt;/code&gt;, which was mentioned earlier. Create a file in your &lt;code&gt;/project&lt;/code&gt; folder named &lt;code&gt;routing.py&lt;/code&gt; and paste the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import re_path
from app import consumers

# URLs that handle the WebSocket connection are placed here.
websocket_urlpatterns=[
                    re_path(
                        r&amp;quot;ws/chat/(?P&amp;lt;chat_box_name&amp;gt;\w+)/$&amp;quot;, consumers.ChatRoomConsumer.as_asgi()
                    ),
                ]

application = ProtocolTypeRouter( 
    {
        &amp;quot;websocket&amp;quot;: AuthMiddlewareStack(
            URLRouter(
               websocket_urlpatterns
            )
        ),
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, let&#x2019;s build the frontend for the application. Create a file in &lt;code&gt;app/templates&lt;/code&gt; with the name &lt;code&gt;chatbox.html&lt;/code&gt;, and then paste the code shown below. Most of this code is the &lt;a href=&quot;https://getbootstrap.com/docs/4.6/getting-started/introduction/#starter-template&quot;&gt;starter template&lt;/a&gt; code from b Bootstrap, just to give the application some styling.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;

&amp;lt;head&amp;gt;
    &amp;lt;!-- Required meta tags --&amp;gt;
    &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1, shrink-to-fit=no&amp;quot;&amp;gt;

    &amp;lt;!-- Bootstrap CSS --&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css&amp;quot;
        integrity=&amp;quot;sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z&amp;quot; crossorigin=&amp;quot;anonymous&amp;quot;&amp;gt;

&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;

    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
        &amp;lt;div class=&amp;quot;row d-flex justify-content-center&amp;quot;&amp;gt;
            &amp;lt;div class=&amp;quot;col-3&amp;quot;&amp;gt;
                &amp;lt;form&amp;gt;
                    &amp;lt;div class=&amp;quot;form-group&amp;quot;&amp;gt;
                        &amp;lt;label for=&amp;quot;exampleFormControlTextarea1&amp;quot; class=&amp;quot;h4 pt-5&amp;quot;&amp;gt;Chatbox&amp;lt;/label&amp;gt;
                        &amp;lt;textarea class=&amp;quot;form-control&amp;quot; id=&amp;quot;chat-text&amp;quot; readonly rows=&amp;quot;10&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&amp;lt;br&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&amp;quot;form-group&amp;quot;&amp;gt;
                        &amp;lt;input class=&amp;quot;form-control&amp;quot; placeholder=&amp;quot;Enter text here&amp;quot; id=&amp;quot;input&amp;quot; type=&amp;quot;text&amp;quot;&amp;gt;&amp;lt;/br&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;input class=&amp;quot;btn btn-primary btn-lg btn-block&amp;quot; id=&amp;quot;submit&amp;quot; type=&amp;quot;button&amp;quot; value=&amp;quot;Send&amp;quot;&amp;gt;
                &amp;lt;/form&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    {% comment %} Get data for username and chatbox name{% endcomment %}
    {{ request.user.username|json_script:&amp;quot;user_username&amp;quot; }}
    {{ chat_box_name|json_script:&amp;quot;room-name&amp;quot; }}
    

    &amp;lt;!-- Optional JavaScript --&amp;gt;
    &amp;lt;!-- jQuery first, then Popper.js, then Bootstrap JS --&amp;gt;
    &amp;lt;script src=&amp;quot;https://code.jquery.com/jquery-3.5.1.slim.min.js&amp;quot;
        integrity=&amp;quot;sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj&amp;quot; crossorigin=&amp;quot;anonymous&amp;quot;&amp;gt;
    &amp;lt;/script&amp;gt;
    &amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js&amp;quot;
        integrity=&amp;quot;sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN&amp;quot; crossorigin=&amp;quot;anonymous&amp;quot;&amp;gt;
    &amp;lt;/script&amp;gt;
    &amp;lt;script src=&amp;quot;https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js&amp;quot;
        integrity=&amp;quot;sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV&amp;quot; crossorigin=&amp;quot;anonymous&amp;quot;&amp;gt;
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The HTML above creates a simple chat interface using Bootstrap for styling. The page features a centered chatbox with a textarea that displays the chat log messages in read-only mode, an input field where users can type their messages, and a send button to submit them. The layout is responsive and uses Bootstrap&apos;s grid system to center a 3-column-wide form on the page. The design is clean and minimal, focusing on functionality with standard Bootstrap components like form controls and a primary button.&lt;/p&gt;
&lt;p&gt;At the bottom of the HTML, there are two Django template tags that inject important data into the page as JSON scripts. These tags capture the current user&apos;s username and the chatbox/chat room name, making them accessible to JavaScript code that will be added later. The page also includes all necessary Bootstrap dependencies (jQuery, Popper.js, and Bootstrap JS) loaded from CDNs. The next step involves adding custom JavaScript code above these script tags to handle WebSocket connections and real-time message functionality, enabling the WebSocket chat application to send and receive messages dynamically.&lt;/p&gt;
&lt;p&gt;Next, we&#x2019;ll develop the JavaScript code that will fetch the data and handle the WebSocket connection from the frontend side. Paste the following code above the first &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in the HTML file that you just created.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;lt;script&amp;gt;
   const user_username = JSON.parse(document.getElementById(&apos;user_username&apos;).textContent);
   document.querySelector(&apos;#submit&apos;).onclick = function (e) {
      const messageInputDom = document.querySelector(&apos;#input&apos;);
      const message = messageInputDom.value;
      chatSocket.send(JSON.stringify({
          &apos;message&apos;: message,
          &apos;username&apos;: user_username,
      }));
      messageInputDom.value = &apos;&apos;;
   };

   const boxName = JSON.parse(document.getElementById(&apos;box-name&apos;).textContent);
   # Create a WebSocket in JavaScript.
   const chatSocket = new WebSocket(
      &apos;ws://&apos; +
      window.location.host +
      &apos;/ws/chat/&apos; +
      boxName +
      &apos;/&apos;
   );

   chatSocket.onmessage = function (e) {
      const data = JSON.parse(e.data);
      document.querySelector(&apos;#chat-text&apos;).value += (data.message + &apos; sent by &apos; + data.username   + &apos;\n&apos;) // add message to text box
   }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This JavaScript code handles the real-time chat functionality by managing WebSocket communication between the client and server. First, it retrieves the current user&apos;s username from the JSON script tag embedded in the HTML and sets up a click event listener on the send button. When the user clicks send, it captures the message from the input field, packages it along with the username into a JSON object, sends it through the WebSocket connection, and then clears the input field for the next message.&lt;/p&gt;
&lt;p&gt;The second part establishes the WebSocket connection itself by extracting the chatbox name from the page and constructing a WebSocket URL using the current host and a specific chat endpoint pattern. It then sets up a message handler that listens for incoming messages from the server. Whenever a new message arrives through the WebSocket, it parses the JSON data and appends the message along with the sender&apos;s username to the chat textarea, creating a simple but functional real-time chat experience where users can see messages as they&apos;re sent.
Now, run the following commands to migrate the authentication model so that you can create new users to test the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can create new users by running the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py createsuperuser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, you can test the chat app by running it and logging in to two users using Django admin. You will be able to do this by logging in to each of the users on different browsers. Then, open the URL &lt;code&gt;127.0.0.1:8000/chat/newbox/&lt;/code&gt; on each of the browsers, and when you send a text, each user receives the text in real time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/django-channels-websockets-chat/final-app.png&quot; alt=&quot;A screenshot of the output of the final Django chat app&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Building on the knowledge gained&lt;/h2&gt;
&lt;p&gt;In this article, you&apos;ve learned the fundamentals of WebSocket technology and why it&#x2019;s essential for building real-time applications like a modern chat app. You saw how Django Channels bridges Django&#x2019;s traditional HTTP request-response model with persistent WebSocket connections, using the Asynchronous Server Gateway Interface (ASGI) to support real-time communication. By building a functional chat room, you gained hands-on experience with consumers, routing via &lt;code&gt;ProtocolTypeRouter&lt;/code&gt;, and managing WebSocket connections from the browser using JavaScript.&lt;/p&gt;
&lt;p&gt;Although we successfully built a real-time chat application, this is just the foundation. One of the most important next steps is adding a database connection to persist chat messages and chat logs, allowing users to retrieve conversation history when they rejoin a chat room. This turns a simple demo into a production-ready chat system. For more performance, replacing the in-memory channel layer with Redis as the message broker is strongly recommended. You could also look into how to &lt;a href=&quot;https://www.honeybadger.io/blog/a-guide-to-exception-handling-in-python/&quot;&gt;handle exception&lt;/a&gt; in the chat app.&lt;/p&gt;
&lt;p&gt;You can further enhance the experience by implementing features such as typing indicators, read receipts, online presence tracking, timestamps for chat messages, and file sharing. At this stage, middleware like &lt;code&gt;AuthMiddlewareStack&lt;/code&gt; becomes important for securing WebSocket connections and ensuring authenticated users can only access authorized chat rooms.&lt;/p&gt;
&lt;p&gt;With these foundations in place, you&#x2019;re well-equipped to build advanced real-time systems and Django chat apps using Django Channels.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The best observability platforms for developers</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/observability-platforms/"/>
    <id>https://www.honeybadger.io/blog/observability-platforms/</id>
    <published>2026-03-10T00:00:00+00:00</published>
    <updated>2026-03-10T00:00:00+00:00</updated>
    <author>
      <name>Julie Kent</name>
    </author>
    <summary type="text">Choosing an observability platform is harder than it should be. This guide breaks down the best options so you can make the right call for your team.</summary>
    <content type="html">&lt;p&gt;At some point, logs stop being enough. As applications grow more distributed, understanding what&apos;s actually happening in production becomes harder. That&apos;s what observability platforms are built for. The hard part is figuring out which one is actually right for your application &#x2014; and your budget.&lt;/p&gt;
&lt;p&gt;This guide covers some popular options: what they do well, where they fall short, and who they&apos;re for. Whether you&apos;re evaluating an observability tool for the first time or reconsidering your current tools, the goal is to give you enough signal to make the right choice.&lt;/p&gt;
&lt;h2&gt;What is an observability platform?&lt;/h2&gt;
&lt;p&gt;Observability tools and platforms are centered around helping engineers understand system performance, visualize data, and ultimately provide comprehensive visibility across the entirety of an organization&apos;s systems.&lt;/p&gt;
&lt;p&gt;Software applications used to be built with monolithic code bases. This means that the entire application shared one code base, one deployable artifact, and one runtime. Everything was tightly coupled together. While yes, there was a need for typical metrics and logs to analyze data and system performance, there weren&apos;t multiple complex systems to aggregate said data sources.&lt;/p&gt;
&lt;p&gt;As the internet continued to take over the world, and more and more complex software companies started to take form, a new paradigm of software architecture emerged. In the early 2010s, the concept of &amp;quot;microservices&amp;quot; started to gather steam as the disadvantages of monoliths, such as slower builds, deployment coupling, and harder to maintain modularity, started to create too much friction across engineering teams.&lt;/p&gt;
&lt;p&gt;In comparison to a monolithic code base, microservices look to break apart business logic into small, independent services that each can be deployed separately. For example, if you were looking to build an e-commerce apparel company, you may want to have separate microservices for orders, inventory, and customers. The advantages of microservices are numerous, especially for apps that need to scale, and include independent scaling (you can put more resources behind ONE service without having to scale the entire system) and deploys, fault isolation (an issue in one microservice may not bring down the entire application), and stack freedom (one app can be written in Swift, one in Rails, etc.).&lt;/p&gt;
&lt;p&gt;However, the existing tools and solutions for observability no longer met the needs of companies utilizing a microservices architecture. With truly distributed systems running in cloud environments, a need for tools that could provide comprehensive insights and visibility started to arise. Software engineering teams needed one unified platform to observe system behavior across all of their infrastructure components.&lt;/p&gt;
&lt;p&gt;For example, let&apos;s go back to the e-commerce apparel company. The engineers need to be able to have unified observability to be able to identify overall system performance bottlenecks across the entire stack. Let&apos;s say the business metric of the number of orders sees a dramatic drop, and the business wants to understand why. A data observability platform might show that an increase in calls to the authentication service is causing increased latency for customers trying to sign in to their accounts, which is causing clients to get frustrated and abandon orders. How does an observability tool capture this telemetry data?&lt;/p&gt;
&lt;p&gt;In software engineering terms, you may hear the term &amp;quot;telemetry data&amp;quot; &#x2014; this typically includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt; (numbers over time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs&lt;/strong&gt; (text events)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traces&lt;/strong&gt; (end-to-end request flows)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt; (discrete actions like deploys, errors, or configuration changes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By bringing together metrics, logs, and traces as observability data, organizations are able to go beyond traditional monitoring to optimize performance, receive real-time, actionable insights, and ultimately reduce operational costs and provide better user interactions.&lt;/p&gt;
&lt;h2&gt;Types of observability platforms&lt;/h2&gt;
&lt;p&gt;Not all observability platforms are solving the same problem. Some platforms are built for complex distributed systems: broad feature coverage, enterprise scale, and pricing to match. Developer-focused tools prioritize fast setup and clear, actionable output, with tighter scope and more predictable costs. Logging platforms are built around ingesting and querying massive volumes of log data. Most teams land somewhere on the spectrum between &amp;quot;we need everything&amp;quot; and &amp;quot;we need something that just works.&amp;quot;&lt;/p&gt;
&lt;p&gt;The features these platforms offer can vary wildly, and the enterprise-focused tools in particular can feel overwhelming. You probably care about a subset of the common features they provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;APM (Application Performance Monitoring)&lt;/strong&gt;: Provides deep visibility into application code across the stack, including features such as distributed tracing, flame graphs, endpoint latency breakdowns, service dependency maps, and profiling of things like CPU, memory, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure and Database Monitoring&lt;/strong&gt;: These observability tools allow engineers to keep tabs on their CPU, memory, containers, cluster health, and network and host-level metrics. Engineers can also glean real-time insights into the database performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Log Management&lt;/strong&gt;: Observability platforms centralize logs across all sources (application, job processor such as Sidekiq, databases such as Postgres, in-memory store such as Redis, and other cloud services). Engineers are able to see live tails (real-time log streaming), log pipelines (filter, transform, mask), and analyze log data to gain comprehensive insights.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Distributed Tracing&lt;/strong&gt;: One of the key components that make observability tools able to provide deeper insights, observability tools allow engineers to easily observe the end-to-end journey of requests and identify bottlenecks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Real User Monitoring (RUM)&lt;/strong&gt;: RUM tooling allows engineers to monitor actual users on their application. For example, an engineer would be able to look up a specific client and view their individual page load times, any errors, and even replay their actual session.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Synthetic Monitoring&lt;/strong&gt;: Whereas RUM tools are tracking real users&apos; behavior, &amp;quot;synthetic&amp;quot; monitoring is exactly what you would expect it to be &#x2014; observability tools simulate user behavior using automated checks like typical API calls and frequent user flows like sign-in, login, etc. This can be very beneficial to continuously monitor the state of the application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Dashboards&lt;/strong&gt;: Oftentimes, it is helpful for engineers to pull insights from multiple tools. For example, an engineer might want to look at the performance of a specific endpoint, alongside business metrics, alongside SLO metrics. Observability tools often provide customizable dashboards so that engineers can gain this comprehensive visibility by creating graphs, time comparisons, and views that can include logs, metrics, and traces.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Alerting &amp;amp; SLO Monitoring&lt;/strong&gt;: These tools help engineers ensure they are being notified of potential issues. For example, observability tools may allow a team to create an alert that pages someone if a Sidekiq queue is greater than a certain amount over a certain time period.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Security Monitoring&lt;/strong&gt;: Observability tools often provide security analytics to identify potential threats or vulnerabilities.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CI (Continuous Integration) Visibility&lt;/strong&gt;: CI is a new-ish software development practice in which teams merge their code changes into a shared main branch, and each merge automatically triggers the build, the test suite to run, any linting or code quality checks, etc. Most observability tools know this is common practice for most dev teams, and provide monitors so that engineers can keep track of pipeline duration, build failures, and metrics related to their test suite &#x2014; for example, how quickly their tests run and any flaky tests (tests that fail or pass sporadically, and are unrelated to the actual code changes being made).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integration Ecosystem&lt;/strong&gt;: In order to make an observability platform the right observability tool for your organization, you need to ensure that it can integrate with all of your existing stack. For example, if you use AWS, you will want to make sure you choose an observability tool that can integrate with AWS. This is, of course, a bit of a trivial example given the popularity of AWS.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Six great observability tools&lt;/h2&gt;
&lt;p&gt;Now that we have a grasp of what observability tools are and why they are important for system performance, especially in distributed systems environments, the next step in our journey is to look at who the players are in this space.&lt;/p&gt;
&lt;h3&gt;1. Datadog&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Overview&lt;/strong&gt;: Arguably the most popular tooling with an incredible overall market share, Datadog is likely a name in this industry that you are already familiar with. Datadog&apos;s centralized platform provides logs, metrics, traces, APM (application performance monitoring), and infrastructure monitoring. It is often the top choice for cloud environments that utilize a heavy microservices architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APM (Application Performance Monitoring)&lt;/li&gt;
&lt;li&gt;Infrastructure and database monitoring&lt;/li&gt;
&lt;li&gt;Log management&lt;/li&gt;
&lt;li&gt;Distributed tracing&lt;/li&gt;
&lt;li&gt;Real user monitoring (RUM)&lt;/li&gt;
&lt;li&gt;Synthetic monitoring&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;li&gt;Alerting &amp;amp; SLO monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Datadog&apos;s pricing model is based on usage and depends on a few different levers, such as the number of hosts, data volume, and features that you choose to integrate. For basic infrastructure monitoring, you are charged per &amp;quot;host&amp;quot; &#x2014; typically around $15 per host per month. If you enable additional features, such as APM, your cost goes up to around $30 per host per month. For other services, pricing is based on how much you use them. For example, per metric, per GB of ingested log data, etc. One of the downsides of Datadog is that its pricing model can be quite complex.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall Pros&lt;/strong&gt;: Trusted by many large companies, strong real-time debugging and dashboarding/visualization tools, full product suite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Can be expensive, and the pricing model itself can be complex, may be overkill for smaller companies.&lt;/p&gt;
&lt;h3&gt;2. New Relic&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt; Another strong player in the observability tools space, New Relic, is also highly sought after by larger companies. It also includes a full product suite that ingests telemetry data (logs, metrics, traces, events) and allows engineers to monitor system performance and visualize data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APM (Application Performance Monitoring)&lt;/li&gt;
&lt;li&gt;Infrastructure and database monitoring&lt;/li&gt;
&lt;li&gt;Log management&lt;/li&gt;
&lt;li&gt;Distributed tracing&lt;/li&gt;
&lt;li&gt;Real user monitoring (RUM)&lt;/li&gt;
&lt;li&gt;Synthetic monitoring&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; New Relic&apos;s pricing is usage-based, centered primarily on data ingestion and user &amp;quot;seats&amp;quot; (i.e, how many users will be utilizing their tools). New Relic does have a free tier, but after that, additional usage is based on GB of data ingested. &amp;quot;Seats&amp;quot; include &amp;quot;full platform users&amp;quot; who need access to advanced observability features, and &amp;quot;basic users&amp;quot; who are free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall Pros:&lt;/strong&gt; Strong integration support (780+), offers &amp;quot;intelligent observability&amp;quot; which utilizes AI to predict issues and automate workflows, and offers a &amp;quot;free tier.&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Utilizes its own query language (NRQL), which creates a steep initial learning curve, and does not have as strong infrastructure tooling as other options.&lt;/p&gt;
&lt;h3&gt;3. Dynatrace&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt; Dynatrace is an enterprise-grade observability and automation platform known for its AI-driven insights. It is often the best observability tool for organizations operating large distributed systems in multi-cloud or Kubernetes-heavy environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APM (Application Performance Monitoring)&lt;/li&gt;
&lt;li&gt;Infrastructure and  database monitoring&lt;/li&gt;
&lt;li&gt;Log management&lt;/li&gt;
&lt;li&gt;Distributed tracing&lt;/li&gt;
&lt;li&gt;Real user monitoring (RUM)&lt;/li&gt;
&lt;li&gt;Synthetic monitoring&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Dynatrace&apos;s pricing model centers around host units and data volume, dependent on which modules are being used. It is one of the more expensive options in the industry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall Pros:&lt;/strong&gt; Less manual setup, known for being &amp;quot;best in class&amp;quot; for AI-assisted root cause analysis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Very expensive, more complex procurement and onboarding.&lt;/p&gt;
&lt;h3&gt;4. Honeybadger&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt; Honeybadger is a developer-focused observability platform built around the things most teams actually need: error tracking, uptime monitoring, cron &amp;amp; heartbeat monitoring, application performance, and log management. Setup takes minutes. You can send structured event data and query it directly, with automatic dashboards and alerting on top. It&apos;s designed to give developers direct access to their data without black-box abstractions or a dedicated ops team to run it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Error and exception tracking&lt;/li&gt;
&lt;li&gt;Uptime and cron/heartbeat monitoring&lt;/li&gt;
&lt;li&gt;Performance insights&lt;/li&gt;
&lt;li&gt;Log management and event tracking&lt;/li&gt;
&lt;li&gt;Querying, dashboards, and alerting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Honeybadger&apos;s pricing is straightforward based on data volume, with no cap on users or hosts. It is typically much more affordable than other observability tools that tier on those dimensions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall Pros:&lt;/strong&gt; Fast, easy setup; strong documentation and integrations with common tools like Slack and GitHub, and simple and transparent pricing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Not the right fit if you&apos;re running a heavily distributed microservices architecture. Deep infrastructure monitoring requires more manual setup compared to dedicated enterprise platforms.&lt;/p&gt;
&lt;h3&gt;5. Splunk&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt; Splunk is another popular option that was originally most known for its log indexing and search, but expanded into a full observability suite that includes all of the typical features. It is known for its deep log management capabilities and ability to handle massive volumes of machine-generated log data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Log Management and Search&lt;/li&gt;
&lt;li&gt;APM (Application Performance Monitoring)&lt;/li&gt;
&lt;li&gt;Infrastructure and Database Monitoring&lt;/li&gt;
&lt;li&gt;Distributed Tracing&lt;/li&gt;
&lt;li&gt;Real User Monitoring (RUM)&lt;/li&gt;
&lt;li&gt;Synthetic Monitoring&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; The pricing varies depending on whether or not your team utilizes their traditional log product or their full observability platform. For their observability tool, pricing is typically based on the number of hosts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall Pros:&lt;/strong&gt; Supports open standards such as OpenTelemetry, highly customizable dashboards, and ties into Splunk&apos;s other offerings if you already utilize them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Higher learning curve given its feature set, still can be cost-prohibitive depending on your host count, often overkill for smaller projects and teams.&lt;/p&gt;
&lt;h3&gt;6. Sentry&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt; Sentry is a developer-focused observability platform that is popular for its error and performance monitoring. It has a strong following with product engineering teams who want to better understand how code changes impact application health and user experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Error Tracking &amp;amp; Alerting&lt;/li&gt;
&lt;li&gt;APM (Application Performance Monitoring)&lt;/li&gt;
&lt;li&gt;Release &amp;amp; Deployment Health Analytics&lt;/li&gt;
&lt;li&gt;Distributed Tracing&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;li&gt;Session &amp;amp; User Impact Visibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Pricing is based on a combination of event-based usage and plan tiers. There is a free tier available. Similar to other platforms, pricing increases as the volume of events grows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall Pros:&lt;/strong&gt; Sentry has a generous free tier that can be utilized for smaller companies or if you just want to try it out. Sentry also offers a strong integration ecosystem and places a heavy emphasis on helping developers accelerate root cause analysis and triaging issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Does not provide a full observability suite like Datadog and New Relic do, so it may not be relevant for larger companies. Their distributed tracing is also not as robust as some of the other players outlined in this article.&lt;/p&gt;
&lt;h2&gt;What to look for in an observability tool&lt;/h2&gt;
&lt;p&gt;So, you may be asking yourself &#x2014; when do I need to start thinking about a data observability tool, and how do I choose? Typically, you may start thinking about choosing an observability tool when you are no longer able to utilize basic logs and ad-hoc debugging to identify and solve problems in production.&lt;/p&gt;
&lt;p&gt;Maybe you have started to utilize background jobs or have introduced asynchronous workflows. Maybe you have started down the path of breaking down a monolithic code base into microservices.&lt;/p&gt;
&lt;p&gt;The main red flag indicating that you may need to add an observability tool to your stack is if you (and your team) start noticing an increasing amount of time that it takes to resolve incidents, and feel like you do not have a true understanding of how your code behaves in production.&lt;/p&gt;
&lt;p&gt;OK, so you have decided that you need a unified observability platform now that traditional monitoring tools are not up to snuff. How do you choose the right fit? Here are some questions to consider:&lt;/p&gt;
&lt;h3&gt;What type of environment does my application run in?&lt;/h3&gt;
&lt;p&gt;For example, if you are running distributed systems with microservices, you will want to prioritize platforms with strong distributed tracing and service dependency mapping.&lt;/p&gt;
&lt;h3&gt;What are my biggest pain points?&lt;/h3&gt;
&lt;p&gt;When considering the most effective observability tool for your team, think about where your problems lie. Is your biggest pain point debugging failures or log management? Then, you may want to focus on a data observability tool with advanced log indexing and search.&lt;/p&gt;
&lt;h3&gt;How big is my team?&lt;/h3&gt;
&lt;p&gt;What observability tool is right for you will also heavily depend on the size of your team. If your team is small, you may want to prioritize tools that are more lightweight and easy to set up. If, however, you are part of a multi-service enterprise organization, you will likely need an observability tool that offers tools that can be used in complex environments and have a strong reputation in the industry &#x2014; the last thing you want to worry about is your observability tool going offline or having bugs of its own.&lt;/p&gt;
&lt;h3&gt;What is my budget?&lt;/h3&gt;
&lt;p&gt;And then, there&apos;s money. Ultimately, while you may love to have the shiniest observability tool with the most advanced feature set, it may not realistically be in your budget. You also need to consider pricing structures... if your application spits out a large amount of log data from lots of different data sources, platforms whose pricing is based on data ingestion may become extremely expensive very quickly.&lt;/p&gt;
&lt;p&gt;Reflecting on the questions above should help point you in the right direction.&lt;/p&gt;
&lt;h2&gt;When is Honeybadger observability the right fit?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/observability-platforms/project-overview-dashboard.png&quot; alt=&quot;Honeybadger&apos;s project overview dashboard, simpler and more actionable than most observability platforms.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We built Honeybadger for teams who are tired of paying for features they don&apos;t use, and want a simpler alternative. If you run a monolith or a reasonable number of distributed services and want visibility into errors, downtime, and application performance &#x2014; without a complex setup or a surprising bill &#x2014; Honeybadger might be the right fit.&lt;/p&gt;
&lt;p&gt;It&apos;s not the right tool if your primary need is deep infrastructure monitoring across a large microservices architecture. For that, Datadog or Dynatrace will serve you better. But if you want something that works out of the box, surfaces actionable issues, and doesn&apos;t require a dedicated team to maintain, &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;give Honeybadger a try&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
</feed>