<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 137Foundry</title>
    <description>The latest articles on DEV Community by 137Foundry (@137foundry).</description>
    <link>https://dev.to/137foundry</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3856342%2F39ac4be7-399f-4f6e-9a32-60abf8a8a324.png</url>
      <title>DEV Community: 137Foundry</title>
      <link>https://dev.to/137foundry</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/137foundry"/>
    <language>en</language>
    <item>
      <title>10 Regex Patterns Every Backend Developer Should Have in Their Snippets Folder</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Tue, 02 Jun 2026 10:03:45 +0000</pubDate>
      <link>https://dev.to/137foundry/10-regex-patterns-every-backend-developer-should-have-in-their-snippets-folder-jgb</link>
      <guid>https://dev.to/137foundry/10-regex-patterns-every-backend-developer-should-have-in-their-snippets-folder-jgb</guid>
      <description>&lt;p&gt;Every backend developer ends up needing the same regex patterns over and over: email validation, URL extraction, number parsing, ID format checks, log parsing. Most rewrite the patterns each time. Most end up with subtle inconsistencies between projects. Keeping a personal snippets file of regex you have actually tested and used is one of those small productivity wins that compounds across years.&lt;/p&gt;

&lt;p&gt;This is a curated list of ten regex patterns that show up in nearly every backend codebase, with the dialect notes and gotchas that matter. Use these as starting points; tune them to your specific inputs before shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pragmatic Email Validation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accepts the vast majority of real email addresses, rejects obviously malformed input, and stays simple enough to avoid backtracking issues. The full RFC 5322 spec is a 6,000-character monster that nobody needs. The &lt;a href="https://www.w3.org/" rel="noopener noreferrer"&gt;W3C HTML specification&lt;/a&gt; for the input type="email" element uses a very similar pragmatic pattern.&lt;/p&gt;

&lt;p&gt;Trade-off: rejects RFC-valid quoted local parts (&lt;code&gt;"foo bar"@example.com&lt;/code&gt;), which essentially no real users ever type. Worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. URL Validation (HTTP/HTTPS Only)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^https?:\/\/([\w-]+\.)+[\w-]+(:\d+)?(\/[\w\-._~:\/?#[\]@!$&amp;amp;'()*+,;=%]*)?$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matches HTTP and HTTPS URLs with optional port and path. The path character class includes the RFC 3986 unreserved and reserved characters that URLs are allowed to contain.&lt;/p&gt;

&lt;p&gt;Gotcha: this validates format only, not reachability. A URL that matches this pattern can still 404 or DNS-fail. For real production validation, use the language's URL parser (&lt;code&gt;URL&lt;/code&gt; in JavaScript, &lt;code&gt;urllib.parse&lt;/code&gt; in Python).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. UUID v4
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validates the UUID v4 format specifically. The &lt;code&gt;4&lt;/code&gt; in the third group and the &lt;code&gt;[89ab]&lt;/code&gt; in the fourth group are version and variant bits required by the v4 spec. A pattern that accepts any 32-hex-with-dashes pattern would also accept v1, v3, and v5 UUIDs.&lt;/p&gt;

&lt;p&gt;For case-insensitive matching of uppercase UUIDs, add the &lt;code&gt;i&lt;/code&gt; flag or expand the character class to &lt;code&gt;[0-9a-fA-F]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ISO 8601 Date
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matches ISO 8601 dates and datetimes, including optional fractional seconds and timezone offsets. Both &lt;code&gt;Z&lt;/code&gt; (UTC) and &lt;code&gt;+05:00&lt;/code&gt;-style offsets are accepted.&lt;/p&gt;

&lt;p&gt;Gotcha: this is format validation only. A pattern-valid date like &lt;code&gt;2026-13-32&lt;/code&gt; is still nonsensical. After regex passes, parse with a real date library to catch semantic errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. US Phone Number (Permissive)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matches US phone numbers in common formats: &lt;code&gt;1234567890&lt;/code&gt;, &lt;code&gt;123-456-7890&lt;/code&gt;, &lt;code&gt;(123) 456-7890&lt;/code&gt;, &lt;code&gt;+1-123-456-7890&lt;/code&gt;. The optional country code and varied separators reflect what users actually type.&lt;/p&gt;

&lt;p&gt;For international phone validation, this pattern is wrong. Use a library like libphonenumber, which handles every country's quirks. Regex alone cannot encode international phone format rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Strong Password Format
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&amp;amp;*]).{8,}$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires at least one lowercase letter, one uppercase letter, one digit, one symbol, and 8+ characters total. Uses positive lookaheads to enforce each requirement independently.&lt;/p&gt;

&lt;p&gt;Modern security guidance from groups like &lt;a href="https://www.nist.gov/" rel="noopener noreferrer"&gt;NIST&lt;/a&gt; and OWASP has shifted away from composition rules in favor of length and breach-list checks, but composition checks remain common in production. Apply this for systems that still require them; do not assume it represents current best practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Hex Color Code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accepts both 3-digit and 6-digit hex colors, with or without the leading &lt;code&gt;#&lt;/code&gt;. Useful for color picker validation, CSS parsing, design tool input.&lt;/p&gt;

&lt;p&gt;For modern CSS, this regex does not cover &lt;code&gt;rgb()&lt;/code&gt;, &lt;code&gt;rgba()&lt;/code&gt;, &lt;code&gt;hsl()&lt;/code&gt;, or named colors. Hex remains the most common format for stored color values, so this pattern handles most cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Whitespace Collapse
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\s+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with the &lt;code&gt;g&lt;/code&gt; flag and &lt;code&gt;.trim()&lt;/code&gt;, this collapses runs of whitespace into single spaces and strips leading and trailing whitespace. The most-used regex in any string-normalization pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gotcha: &lt;code&gt;\s&lt;/code&gt; does not match all Unicode whitespace in JavaScript regex. For text that may include non-breaking spaces, em spaces, or ideographic spaces, use a broader character class.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Extract Markdown Link
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\[([^\]]+)\]\(([^)]+)\)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Captures the link text (group 1) and URL (group 2) from standard markdown links. Useful for processing markdown content programmatically, building link analyzers, or migrating content between formats.&lt;/p&gt;

&lt;p&gt;Breaks on links with literal parentheses in the URL (anything where the URL itself contains an unescaped closing parenthesis confuses the simple character class). For full markdown parsing, a real markdown parser is the right tool. This pattern works for the standard cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Pull Numbers From Text
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-?\d+(\.\d+)?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matches signed integers and decimals. Useful for log parsing, financial data extraction, or any context where mixed text and numbers need separating.&lt;/p&gt;

&lt;p&gt;For numbers with thousand separators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-?\d{1,3}(,\d{3})*(\.\d+)?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For numbers in scientific notation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-?\d+(\.\d+)?([eE][+-]?\d+)?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the variant that matches your input format. The most common bug is using the basic pattern on data that includes thousand separators and getting truncated values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Trim and Normalize in One Pass
&lt;/h2&gt;

&lt;p&gt;A useful idiom that combines patterns 8 and others: trim leading and trailing whitespace and collapse internal whitespace in a single operation. In JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows up constantly in user-input normalization paths. Worth memorizing rather than rewriting each time.&lt;/p&gt;

&lt;p&gt;A related pattern: stripping zero-width characters that sneak in via copy-paste from word processors and PDFs. Add &lt;code&gt;[​-‍﻿]&lt;/code&gt; to your normalization regex to remove zero-width spaces, joiners, and BOMs. These cause real bugs in downstream systems that compare strings byte-for-byte, and they are invisible in most editors.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Maintain These Patterns
&lt;/h2&gt;

&lt;p&gt;A snippets file is only useful if you maintain it. A few practices that keep it useful over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One file per language&lt;/strong&gt; or one file with language-tagged sections. JavaScript regex literals and Python raw strings have different syntax but the same patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments on every pattern&lt;/strong&gt; explaining what it accepts, what it rejects, and what it does not try to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linked test cases&lt;/strong&gt; somewhere accessible (a gist, a repo, a tests file in your dotfiles).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A changelog&lt;/strong&gt; of when patterns were updated and why. The change history is valuable when you wonder why a pattern looks the way it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pair the snippets file with regex testing tools like &lt;a href="https://regex101.com/" rel="noopener noreferrer"&gt;regex101.com&lt;/a&gt; for testing modifications before saving them back. The combination of a tested snippets library and an interactive tester turns regex from a guessing game into a quick lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  When These Patterns Are Not Enough
&lt;/h2&gt;

&lt;p&gt;These ten cover the most common needs but not every need. Cases where you need to reach beyond simple regex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internationalized inputs.&lt;/strong&gt; Unicode-aware regex is harder than ASCII regex. Use Unicode property escapes (&lt;code&gt;\p{L}&lt;/code&gt; for any letter) where the dialect supports them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive structures.&lt;/strong&gt; Nested HTML, balanced brackets, nested function calls. Regex cannot parse these correctly. Use a real parser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context-sensitive validation.&lt;/strong&gt; "Valid if this other field equals X" requires more than regex. Use a schema validator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance-sensitive paths.&lt;/strong&gt; Compile patterns once and reuse them. In Python, &lt;code&gt;re.compile()&lt;/code&gt;. In Java, &lt;code&gt;Pattern.compile()&lt;/code&gt;. In JavaScript, the literal form is automatically cached but &lt;code&gt;new RegExp(...)&lt;/code&gt; inside a loop is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production data validation work at 137Foundry, these patterns are the starting point of a layered approach: cheap regex format checks at the boundary, then more expensive validation layers that handle the cases regex cannot.&lt;/p&gt;

&lt;p&gt;For more on the validation patterns that go around these regex snippets in production systems, see the full article &lt;a href="https://137foundry.com/articles/regex-code-snippets-for-common-validation-and-parsing" rel="noopener noreferrer"&gt;Regex Code Snippets: Patterns for Common Validation and Parsing Problems&lt;/a&gt;. The &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;137Foundry data integration service&lt;/a&gt; covers the architectural side of validation in production pipelines, and the &lt;a href="https://137foundry.com/services" rel="noopener noreferrer"&gt;services hub&lt;/a&gt; describes related integration and automation work.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Write Regex Patterns That Survive Real-World Input</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Tue, 02 Jun 2026 10:02:05 +0000</pubDate>
      <link>https://dev.to/137foundry/how-to-write-regex-patterns-that-survive-real-world-input-1b5g</link>
      <guid>https://dev.to/137foundry/how-to-write-regex-patterns-that-survive-real-world-input-1b5g</guid>
      <description>&lt;p&gt;A regex that works on test data is a hypothesis. A regex that works on production data is an answer. Most developers do not appreciate this distinction until they ship a parser that processes 95 percent of inputs correctly and crashes on the other 5 percent at 2 AM.&lt;/p&gt;

&lt;p&gt;This is a step-by-step approach to writing regex patterns that hold up against real-world input, including the categories of input that test data rarely covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Collect Real Input Before Writing the Pattern
&lt;/h2&gt;

&lt;p&gt;The single biggest mistake in regex design is writing the pattern first and looking at real data second. The correct order is the opposite: collect a meaningful sample of actual input, look at the variations, and only then write a pattern that handles them.&lt;/p&gt;

&lt;p&gt;For an email validator, sample input from your actual user signups (if any exist). For a date parser, sample the dates from the actual document corpus. For a CSV splitter, sample the actual CSV files you need to process.&lt;/p&gt;

&lt;p&gt;The variations you find are almost always wider than you would have guessed. Real CSV files have inconsistent quoting. Real dates include misspellings. Real URLs include trailing whitespace, mixed case, and trailing punctuation from the surrounding text. Real names include hyphens, apostrophes, periods, and Unicode characters.&lt;/p&gt;

&lt;p&gt;A regex written without this sample is a regex written against an imaginary input distribution. It will fail on the real distribution in proportion to how much the real distribution differs from the imagined one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Anchor Decisively
&lt;/h2&gt;

&lt;p&gt;The most common cause of regex bugs that pass tests but fail in production is missing anchors. A pattern without &lt;code&gt;^&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; matches substrings, which means a "valid email" regex will accept &lt;code&gt;random text and then user@example.com inside it&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Decide explicitly whether the pattern should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Match the entire string (&lt;code&gt;^pattern$&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Match any substring (&lt;code&gt;pattern&lt;/code&gt; with no anchors)&lt;/li&gt;
&lt;li&gt;Match at word boundaries (&lt;code&gt;\bpattern\b&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Match from the start but not require the end (&lt;code&gt;^pattern&lt;/code&gt; without &lt;code&gt;$&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is appropriate in different contexts. None is the right default in all cases. The single biggest source of bugs is people writing un-anchored patterns when they meant fully-anchored ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Escape Literally Everything Special
&lt;/h2&gt;

&lt;p&gt;Regex special characters in a pattern that should match them literally need escaping. The list of special characters in most dialects: &lt;code&gt;. * + ? ^ $ ( ) [ ] { } | \ /&lt;/code&gt;. Plus &lt;code&gt;-&lt;/code&gt; inside character classes when used as a range.&lt;/p&gt;

&lt;p&gt;The dot is the most commonly missed escape. &lt;code&gt;example.com&lt;/code&gt; in a regex matches strings like &lt;code&gt;exampleXcom&lt;/code&gt; because the dot matches any character. Almost always, when you write a domain in a regex, you want &lt;code&gt;example\.com&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Languages that support raw regex strings (Python's &lt;code&gt;r"..."&lt;/code&gt;, JavaScript's literal &lt;code&gt;/.../&lt;/code&gt;, Ruby's &lt;code&gt;%r{...}&lt;/code&gt;) make this easier because you do not have to double-escape backslashes. Languages that require regex in regular strings double-escape: &lt;code&gt;"\\d+"&lt;/code&gt; for the same pattern that &lt;code&gt;r"\d+"&lt;/code&gt; produces. The double-escaping is one of the easiest sources of subtle bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make Greedy vs Non-Greedy Choices Explicit
&lt;/h2&gt;

&lt;p&gt;A regex like &lt;code&gt;&amp;lt;.*&amp;gt;&lt;/code&gt; is greedy: it matches from the first &lt;code&gt;&amp;lt;&lt;/code&gt; to the last &lt;code&gt;&amp;gt;&lt;/code&gt;. A regex like &lt;code&gt;&amp;lt;.*?&amp;gt;&lt;/code&gt; is non-greedy: it matches each &lt;code&gt;&amp;lt;...&amp;gt;&lt;/code&gt; pair individually.&lt;/p&gt;

&lt;p&gt;Greedy is the default in almost all dialects. Most of the time, this is the wrong default for what people actually want.&lt;/p&gt;

&lt;p&gt;The rule of thumb: if you are extracting tagged content, you almost always want non-greedy. If you are validating a single token, the choice does not matter because anchors will make the question moot.&lt;/p&gt;

&lt;p&gt;A useful test: when the regex matches against an input with multiple instances of the pattern, does it return one large match or several small ones? If the answer is unexpected, the greedy vs non-greedy choice is probably wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test Boundary Inputs Systematically
&lt;/h2&gt;

&lt;p&gt;Once the pattern is written, test it against these categories of input deliberately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty input.&lt;/strong&gt; Does the pattern reject empty strings appropriately, or does it accept them when it should not?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitespace-only input.&lt;/strong&gt; Spaces, tabs, newlines, and especially mixtures of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input at exactly the boundary.&lt;/strong&gt; If the pattern allows 1 to 50 characters, test 0, 1, 50, and 51.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input one character over the boundary.&lt;/strong&gt; Common off-by-one errors show up here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input with leading or trailing whitespace.&lt;/strong&gt; Real users paste from PDFs and word processors that include invisible characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input with mixed line endings.&lt;/strong&gt; &lt;code&gt;\r\n&lt;/code&gt; vs &lt;code&gt;\n&lt;/code&gt; vs &lt;code&gt;\r&lt;/code&gt; causes parsing bugs in regex that uses &lt;code&gt;.&lt;/code&gt; (which usually does not match newlines) without thinking about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input with Unicode characters.&lt;/strong&gt; Cyrillic, Chinese, emoji, mathematical symbols. The pattern's behavior on these is usually surprising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input that should fail.&lt;/strong&gt; Make a deliberately malformed version of the expected input and confirm the pattern rejects it.&lt;/p&gt;

&lt;p&gt;A pattern that passes all these tests is much more likely to survive production than one that passes only the happy-path tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Use a Real Regex Tester
&lt;/h2&gt;

&lt;p&gt;Manual testing in code is slow and lossy. Real regex testers like &lt;a href="https://regex101.com/" rel="noopener noreferrer"&gt;regex101.com&lt;/a&gt; and &lt;a href="https://regexr.com/" rel="noopener noreferrer"&gt;regexr.com&lt;/a&gt; show the pattern matching live against input, with explanations of each token, capture group highlighting, and step-count metrics.&lt;/p&gt;

&lt;p&gt;The step counter is especially valuable because it reveals catastrophic backtracking before it hits production. A pattern that takes 100 steps on a 50-character input is fine. A pattern that takes 100,000 steps on a 60-character input has a backtracking problem and should be rewritten.&lt;/p&gt;

&lt;p&gt;Most regex testers also explain what each part of the pattern matches in plain language, which is a useful sanity check when reading regex written by someone else (or by you six months ago).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Watch for Catastrophic Backtracking
&lt;/h2&gt;

&lt;p&gt;Certain regex constructs can become exponentially slow on certain inputs. The classic case is nested quantifiers like &lt;code&gt;(a+)+&lt;/code&gt; against a long string of &lt;code&gt;a&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;Defenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid nested quantifiers when possible. &lt;code&gt;(a+)+&lt;/code&gt; is almost always equivalent to &lt;code&gt;a+&lt;/code&gt; and is much safer.&lt;/li&gt;
&lt;li&gt;Use possessive quantifiers (&lt;code&gt;*+&lt;/code&gt;, &lt;code&gt;++&lt;/code&gt;) or atomic groups in regex dialects that support them. JavaScript does not; Python and PCRE do.&lt;/li&gt;
&lt;li&gt;Cap input length before applying regex to user-controlled input.&lt;/li&gt;
&lt;li&gt;Test against pathological inputs deliberately, especially for regex that processes untrusted input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For security-sensitive contexts, &lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN documentation&lt;/a&gt; and OWASP resources both cover ReDoS (regex denial of service) and the patterns to avoid. The short version: if the input is attacker-controlled and the regex has backtracking risk, treat it as a vulnerability and rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Add Comments and Test Cases
&lt;/h2&gt;

&lt;p&gt;A regex pattern with no documentation is unreadable six months later, even to the person who wrote it. Two practical mitigations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the &lt;code&gt;x&lt;/code&gt; flag where supported&lt;/strong&gt; (Python and PCRE call this verbose mode). This lets you write regex on multiple lines with comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    ^                 # start
    [A-Za-z0-9._%+-]+ # local part
    @                 # at sign
    [A-Za-z0-9.-]+    # domain
    \.[A-Za-z]{2,}    # TLD
    $                 # end
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is the same, but a human can read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintain test cases as living documentation.&lt;/strong&gt; A test file with a list of valid and invalid inputs (with comments explaining each case) is more useful than any amount of inline regex documentation. It also fails loudly when someone refactors the regex incorrectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Plan for the Regex to Be Wrong
&lt;/h2&gt;

&lt;p&gt;Even careful regex will sometimes be wrong. The right architectural defense is to make wrongness recoverable.&lt;/p&gt;

&lt;p&gt;In data pipelines, this means logging the inputs that fail the regex (for later analysis), not just rejecting them silently. In user-facing forms, this means providing clear error messages so the user can fix the input. In imports, this means rejecting the failing row but continuing with the rest of the file, not crashing the whole import.&lt;/p&gt;

&lt;p&gt;A regex that is occasionally wrong but reports its wrongness clearly is much more operationally useful than one that is occasionally wrong and silently corrupts downstream data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;A regex pattern that survives production is one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was written against real sample input, not imagined input&lt;/li&gt;
&lt;li&gt;Anchors decisively&lt;/li&gt;
&lt;li&gt;Escapes special characters consistently&lt;/li&gt;
&lt;li&gt;Handles greedy vs non-greedy explicitly&lt;/li&gt;
&lt;li&gt;Has been tested against boundary cases&lt;/li&gt;
&lt;li&gt;Has been benchmarked for backtracking risk&lt;/li&gt;
&lt;li&gt;Is documented for future readers&lt;/li&gt;
&lt;li&gt;Operates inside a system that can recover from being wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full reference on patterns we use for common validation and parsing problems is in &lt;a href="https://137foundry.com/articles/regex-code-snippets-for-common-validation-and-parsing" rel="noopener noreferrer"&gt;Regex Code Snippets: Patterns for Common Validation and Parsing Problems&lt;/a&gt; on our site at &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;https://137foundry.com&lt;/a&gt;. The structural advice above is what makes those patterns actually work in production rather than just in test files.&lt;/p&gt;

&lt;p&gt;For production data validation work, our &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration service&lt;/a&gt; covers the architectural patterns that go around regex use in messy real-world data flows.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Implementing Change Data Capture for Reliable Bi-Directional Data Sync</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Mon, 01 Jun 2026 11:32:19 +0000</pubDate>
      <link>https://dev.to/137foundry/implementing-change-data-capture-for-reliable-bi-directional-data-sync-2jlj</link>
      <guid>https://dev.to/137foundry/implementing-change-data-capture-for-reliable-bi-directional-data-sync-2jlj</guid>
      <description>&lt;p&gt;Before you can sync data between two systems, you need a reliable way to know what changed. Change Data Capture (CDC) is the pattern for detecting changes as they happen rather than scanning entire tables on every sync cycle. Without CDC, the sync layer must do expensive full-table comparisons or risk missing changes entirely.&lt;/p&gt;

&lt;p&gt;There are two main approaches: database-level CDC that reads from the write-ahead log, and application-level CDC that uses timestamps and polling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Database-Level CDC (PostgreSQL WAL)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/logical-replication.html" rel="noopener noreferrer"&gt;PostgreSQL logical replication&lt;/a&gt; allows consumers to subscribe to a stream of row-level changes directly from the database write-ahead log (WAL). Every insert, update, and delete is captured with before and after values.&lt;/p&gt;

&lt;p&gt;Setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- postgresql.conf must have wal_level = logical&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;PUBLICATION&lt;/span&gt; &lt;span class="n"&gt;sync_pub&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_create_logical_replication_slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sync_slot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pgoutput'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python consumer using psycopg2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2.extras&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_repl_conn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;connection_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogicalReplicationConnection&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume_changes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_repl_conn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_replication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;slot_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proto_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publication_names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# I=insert, U=update, D=delete
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;U&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
            &lt;span class="c1"&gt;# emit {action: "upsert", table: ..., record: ...}
&lt;/span&gt;        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
            &lt;span class="c1"&gt;# emit {action: "delete", table: ..., identity: ...}
&lt;/span&gt;        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flush_lsn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WAL-based CDC captures every change including hard deletes. The main operational concern is replication slot lag: if the consumer falls behind, the slot prevents WAL segments from being recycled, potentially filling disk. Monitor &lt;code&gt;pg_replication_slots&lt;/code&gt; for &lt;code&gt;lag_bytes&lt;/code&gt; and alert at 500 MB.&lt;/p&gt;

&lt;p&gt;Alternatively, &lt;a href="https://debezium.io/" rel="noopener noreferrer"&gt;Debezium&lt;/a&gt; provides a managed CDC layer on top of PostgreSQL that handles slot management, schema evolution, and failure recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Application-Level CDC (Timestamp Polling)
&lt;/h2&gt;

&lt;p&gt;Timestamp polling queries for records where &lt;code&gt;updated_at &amp;gt; last_processed_timestamp&lt;/code&gt;. This is simpler to set up and works with any database, but cannot detect hard deletes.&lt;/p&gt;

&lt;p&gt;Schema requirement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- soft deletes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_customers_updated_at&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poller with persistent high-watermark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;redis_lib&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TimestampCDCPoller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wm_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync:watermark:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_watermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wm_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_watermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wm_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# 10-second overlap buffer for rows arriving slightly out of order
&lt;/span&gt;        &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_watermark&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE updated_at &amp;gt; :since ORDER BY updated_at ASC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;since&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_watermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_mapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 10-second overlap means some rows are processed twice -- which is why idempotent consumers are essential. Store the watermark in Redis (persistent across restarts) rather than in memory. An in-memory watermark is lost on consumer restart, causing either a full re-scan or missed changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering Sync-Originated Changes
&lt;/h2&gt;

&lt;p&gt;Both CDC approaches will detect changes made by your sync layer itself, causing sync loops. Tag the sync connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET application_name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data_sync&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE customers SET name = :name WHERE id = :id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your WAL consumer, skip events where &lt;code&gt;application_name = 'data_sync'&lt;/code&gt;. For timestamp polling, add a &lt;code&gt;synced_at&lt;/code&gt; column and skip rows where &lt;code&gt;updated_at - synced_at &amp;lt; INTERVAL '2 seconds'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Approach
&lt;/h2&gt;

&lt;p&gt;Use WAL-based CDC when: you need capture of hard deletes, you have high change volume (over 1,000 rows per minute), or you need sub-second sync latency. Use &lt;a href="https://debezium.io/" rel="noopener noreferrer"&gt;Debezium&lt;/a&gt; for production systems with high reliability requirements.&lt;/p&gt;

&lt;p&gt;Use timestamp polling when: you cannot modify database replication configuration (common in managed database services like AWS RDS), you need a simpler operational setup, or sync latency tolerance is 30 seconds or more.&lt;/p&gt;

&lt;p&gt;For the full guide including conflict resolution and dead-letter queue setup, see &lt;a href="https://137foundry.com/articles/how-to-build-bidirectional-data-sync-business-applications" rel="noopener noreferrer"&gt;How to Build a Bi-Directional Data Sync Between Business Applications&lt;/a&gt;. For production implementation support, the &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;137Foundry data integration services&lt;/a&gt; team has run both CDC patterns across multiple client systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring CDC Consumers in Production
&lt;/h2&gt;

&lt;p&gt;Regardless of which CDC approach you use, the operational monitoring for the consumer follows the same pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer lag.&lt;/strong&gt; The gap between the most recent change event captured and the most recent change event processed. For WAL-based CDC, this is visible as replication slot lag in &lt;code&gt;pg_replication_slots&lt;/code&gt;. For timestamp polling, it is the difference between &lt;code&gt;NOW()&lt;/code&gt; and the current high-watermark.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer heartbeat.&lt;/strong&gt; For consumers running as background processes, a heartbeat check verifies that the consumer is still running and processing events. A consumer that has crashed but has not been restarted is invisible without an explicit heartbeat check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error rate.&lt;/strong&gt; The fraction of processed events that result in errors (schema mismatch, downstream API failures, constraint violations). A rising error rate indicates something changed in the upstream system or downstream dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DLQ depth.&lt;/strong&gt; For conflicts and permanent failures routed to a dead-letter queue, the DLQ depth should remain low (typically under 10 entries in a healthy system). A growing DLQ means problems are accumulating without resolution.&lt;/p&gt;

&lt;p&gt;These four metrics -- lag, heartbeat, error rate, DLQ depth -- are the minimum viable monitoring for any CDC-based sync system. Without them, the sync can fail silently for hours before anyone notices.&lt;/p&gt;

&lt;p&gt;For the complete guide on building reliable bi-directional sync including CDC implementation and operational monitoring, see &lt;a href="https://137foundry.com/articles/how-to-build-bidirectional-data-sync-business-applications" rel="noopener noreferrer"&gt;137Foundry's data integration resources&lt;/a&gt; and the &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration services overview&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Production Reliability
&lt;/h2&gt;

&lt;p&gt;The failure modes of bi-directional sync are almost always discovered in production, not in testing. Test environments rarely replicate the exact conditions that cause clock skew conflicts -- clock synchronization on development machines is generally better than on production infrastructure. Test environments rarely replicate the specific bulk operation patterns that create consistency gaps. And test environments rarely run long enough to reveal the slow drift that accumulates when a field authority map is not updated after a schema change.&lt;/p&gt;

&lt;p&gt;This is not an argument against testing -- it is an argument for investing in observability alongside testing. The monitoring patterns in this guide (sync lag, DLQ depth, conflict rate, record count parity) give you visibility into problems that tests will not catch before they affect users.&lt;/p&gt;

&lt;p&gt;For teams building a bi-directional sync for the first time, the practical recommendation is: build the operational baseline (DLQ, monitoring, idempotency, loop prevention) before the first production deployment, not after the first production incident. The upfront cost is modest. The incident prevention value is significant.&lt;/p&gt;

&lt;p&gt;For technical implementation guidance, see &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; and the &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration resources&lt;/a&gt;. For production architecture review and implementation support, the &lt;a href="https://137foundry.com/services" rel="noopener noreferrer"&gt;137Foundry services team&lt;/a&gt; works with teams across the integration lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Production Reliability
&lt;/h2&gt;

&lt;p&gt;The failure modes of bi-directional sync are almost always discovered in production, not in testing. Test environments rarely replicate the exact conditions that cause clock skew conflicts -- clock synchronization on development machines is generally better than on production infrastructure. Test environments rarely replicate the specific bulk operation patterns that create consistency gaps. And test environments rarely run long enough to reveal the slow drift that accumulates when a field authority map is not updated after a schema change.&lt;/p&gt;

&lt;p&gt;This is not an argument against testing -- it is an argument for investing in observability alongside testing. The monitoring patterns in this guide (sync lag, DLQ depth, conflict rate, record count parity) give you visibility into problems that tests will not catch before they affect users.&lt;/p&gt;

&lt;p&gt;For teams building a bi-directional sync for the first time, the practical recommendation is: build the operational baseline (DLQ, monitoring, idempotency, loop prevention) before the first production deployment, not after the first production incident. The upfront cost is modest. The incident prevention value is significant.&lt;/p&gt;

&lt;p&gt;For technical implementation guidance, see &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; and the &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration resources&lt;/a&gt;. For production architecture review and implementation support, the &lt;a href="https://137foundry.com/services" rel="noopener noreferrer"&gt;137Foundry services team&lt;/a&gt; works with teams across the integration lifecycle.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Conflict Resolution in Bi-Directional Data Sync: Strategies That Hold Up in Production</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Mon, 01 Jun 2026 11:32:18 +0000</pubDate>
      <link>https://dev.to/137foundry/conflict-resolution-in-bi-directional-data-sync-strategies-that-hold-up-in-production-4moo</link>
      <guid>https://dev.to/137foundry/conflict-resolution-in-bi-directional-data-sync-strategies-that-hold-up-in-production-4moo</guid>
      <description>&lt;p&gt;In one-way data pipelines, conflict resolution is not a concept -- there is one authoritative source. In bi-directional sync, both systems write to shared data, and the sync layer must decide what to do when both systems have updated the same record since the last sync cycle. Getting this wrong means silently dropping updates, silently overwriting updates, or accumulating conflicts that gradually corrupt your data.&lt;/p&gt;

&lt;p&gt;This covers three conflict resolution strategies with Python implementation examples, and the operational setup that keeps them working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: Last-Write-Wins with Clock Skew Tolerance
&lt;/h2&gt;

&lt;p&gt;Last-write-wins compares &lt;code&gt;updated_at&lt;/code&gt; timestamps and applies the more recent version. Simple in theory; fragile in practice because of clock skew.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Clock_skew" rel="noopener noreferrer"&gt;Clock skew&lt;/a&gt; between servers in a distributed system can reach hundreds of milliseconds to several seconds. &lt;a href="https://en.wikipedia.org/wiki/Network_Time_Protocol" rel="noopener noreferrer"&gt;NTP&lt;/a&gt; synchronization reduces drift but does not eliminate it. A last-write-wins resolution will produce the wrong answer whenever two changes arrive within the skew window -- and the failure is silent.&lt;/p&gt;

&lt;p&gt;The mitigation: add a tolerance window. Conflicts where the timestamp gap is smaller than the expected clock skew get routed to a dead-letter queue for review rather than being resolved automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="n"&gt;CLOCK_SKEW_TOLERANCE_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;source_system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_last_write_wins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;record_b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tolerance_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CLOCK_SKEW_TOLERANCE_MS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Returns None if conflict is ambiguous (within clock skew tolerance)
&lt;/span&gt;    &lt;span class="n"&gt;delta_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;record_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;tolerance_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# route to DLQ
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;record_a&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;record_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;record_b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test this across the full range of timestamp gaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_within_tolerance_routes_to_dlq&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rec_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rec_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;milliseconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;resolve_last_write_wins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_outside_tolerance_resolves_correctly&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rec_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rec_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_last_write_wins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_system&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Strategy 2: Field-Authority Mapping
&lt;/h2&gt;

&lt;p&gt;Field authority designates each field as owned by a specific system. Only the owning system's value is authoritative for that field. This eliminates conflicts entirely for fields with a clear owner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="n"&gt;FieldAuthority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;FIELD_AUTHORITY_MAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FieldAuthority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# CRM owns contact info
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Billing owns payment data
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing_address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# Both systems can update
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_activity_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge_with_field_authority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;local_record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;incoming_record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Apply fields from incoming where source_system is authoritative.
&lt;/span&gt;    &lt;span class="c1"&gt;# Fields not in the map are logged and skipped.
&lt;/span&gt;    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;incoming_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;authority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FIELD_AUTHORITY_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Field &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not in authority map -- skipping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;source_system&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key operational concern: the &lt;code&gt;FIELD_AUTHORITY_MAP&lt;/code&gt; must be updated whenever either system adds a new field. Store it in a configuration file rather than hard-coding it, so updates do not require a code deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 3: Hybrid with Dead-Letter Queue
&lt;/h2&gt;

&lt;p&gt;Most production bi-directional syncs combine last-write-wins for shared fields, field authority for owned fields, and a DLQ for conflicts that cannot be resolved deterministically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;dlq_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_to_dlq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conflict_event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conflict_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conflict_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;dlq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync:dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SyncRecord&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Try last-write-wins first
&lt;/span&gt;    &lt;span class="n"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_last_write_wins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;winner&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;winner&lt;/span&gt;
    &lt;span class="c1"&gt;# Cannot resolve -- route to DLQ
&lt;/span&gt;    &lt;span class="nf"&gt;route_to_dlq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Within clock skew tolerance -- manual review required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;record_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring Conflict Rates in Production
&lt;/h2&gt;

&lt;p&gt;A healthy bi-directional sync has a low but non-zero conflict rate. Spikes in conflict rate indicate both systems are being written to simultaneously more than expected -- usually a sign of a bug in upstream application logic or a runaway batch process writing to both systems at the same time.&lt;/p&gt;

&lt;p&gt;Track conflict rate (conflicts per 1,000 sync events) and DLQ depth as primary operational metrics. Alert when conflict rate increases by more than 2x week-over-week, and alert immediately when DLQ depth exceeds a threshold. Without these alerts, conflicts accumulate silently and surface only when a user reports that their update was overwritten.&lt;/p&gt;

&lt;p&gt;For the full architecture guide including CDC setup, sync loop prevention, and operational monitoring, see &lt;a href="https://137foundry.com/articles/how-to-build-bidirectional-data-sync-business-applications" rel="noopener noreferrer"&gt;How to Build a Bi-Directional Data Sync Between Business Applications&lt;/a&gt;. The &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration work at 137Foundry&lt;/a&gt; covers these patterns in production across multiple client integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  DLQ Review Process in Practice
&lt;/h2&gt;

&lt;p&gt;A dead-letter queue that nobody reviews is worse than no DLQ at all -- it creates a false sense of safety while conflicts accumulate. The DLQ needs a human review process, and that process should be defined before the first DLQ entry arrives.&lt;/p&gt;

&lt;p&gt;The minimum viable DLQ review process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alert on DLQ growth.&lt;/strong&gt; When DLQ depth exceeds a threshold (e.g., 10 entries), alert the on-call team. Do not wait for a daily review; real conflicts should be resolved within hours.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review interface.&lt;/strong&gt; A simple web interface or CLI tool that shows each DLQ entry with the record ID, both conflicting versions, the conflict reason, and the timestamp. Buttons or commands to apply record A, apply record B, or dismiss (if the conflict is no longer relevant).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit trail.&lt;/strong&gt; Every DLQ resolution action is logged with the timestamp, the reviewer, and the chosen resolution. This audit trail is essential for debugging if the resolution was wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pattern detection.&lt;/strong&gt; Periodically review the DLQ for patterns: the same record appearing multiple times, the same fields always conflicting, the same source system always losing. Patterns in DLQ entries indicate structural issues in the conflict resolution rules that should be fixed, not just resolved case-by-case.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://www.sqlalchemy.org/" rel="noopener noreferrer"&gt;SQLAlchemy&lt;/a&gt; ORM works well for the audit trail tables. &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; sorted sets are convenient for the DLQ itself -- scores by timestamp allow querying the oldest unresolved entries first.&lt;/p&gt;

&lt;p&gt;For the full architecture guide covering conflict resolution, CDC, and operational monitoring, see &lt;a href="https://137foundry.com/articles/how-to-build-bidirectional-data-sync-business-applications" rel="noopener noreferrer"&gt;How to Build a Bi-Directional Data Sync Between Business Applications&lt;/a&gt;. The &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration work at 137Foundry&lt;/a&gt; covers these patterns across multiple production integration projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Production Reliability
&lt;/h2&gt;

&lt;p&gt;The failure modes of bi-directional sync are almost always discovered in production, not in testing. Test environments rarely replicate the exact conditions that cause clock skew conflicts -- clock synchronization on development machines is generally better than on production infrastructure. Test environments rarely replicate the specific bulk operation patterns that create consistency gaps. And test environments rarely run long enough to reveal the slow drift that accumulates when a field authority map is not updated after a schema change.&lt;/p&gt;

&lt;p&gt;This is not an argument against testing -- it is an argument for investing in observability alongside testing. The monitoring patterns in this guide (sync lag, DLQ depth, conflict rate, record count parity) give you visibility into problems that tests will not catch before they affect users.&lt;/p&gt;

&lt;p&gt;For teams building a bi-directional sync for the first time, the practical recommendation is: build the operational baseline (DLQ, monitoring, idempotency, loop prevention) before the first production deployment, not after the first production incident. The upfront cost is modest. The incident prevention value is significant.&lt;/p&gt;

&lt;p&gt;For technical implementation guidance, see &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; and the &lt;a href="https://137foundry.com/services/data-integration" rel="noopener noreferrer"&gt;data integration resources&lt;/a&gt;. For production architecture review and implementation support, the &lt;a href="https://137foundry.com/services" rel="noopener noreferrer"&gt;137Foundry services team&lt;/a&gt; works with teams across the integration lifecycle.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Prevent the Flash of Wrong Theme When Implementing Dark Mode</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Sun, 31 May 2026 10:39:08 +0000</pubDate>
      <link>https://dev.to/137foundry/how-to-prevent-the-flash-of-wrong-theme-when-implementing-dark-mode-2pg1</link>
      <guid>https://dev.to/137foundry/how-to-prevent-the-flash-of-wrong-theme-when-implementing-dark-mode-2pg1</guid>
      <description>&lt;p&gt;The flash of wrong theme (FOWT) is the brief moment where a page loads in light mode for a user who prefers dark, or vice versa, before JavaScript applies the correct theme. It happens because the default page color scheme renders before the JavaScript that reads localStorage and applies the user's preference has a chance to run.&lt;/p&gt;

&lt;p&gt;For many applications, FOWT is the most visible dark mode bug and the hardest to eliminate without understanding what causes it. This article explains the root cause and walks through the specific fix for client-rendered applications, server-rendered applications, and Next.js specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Flash Happens
&lt;/h2&gt;

&lt;p&gt;JavaScript-first dark mode implementations typically follow this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Browser receives HTML and begins parsing&lt;/li&gt;
&lt;li&gt;Browser begins downloading and evaluating scripts (deferred or at end of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CSS renders the page using &lt;code&gt;:root&lt;/code&gt; default values (light mode)&lt;/li&gt;
&lt;li&gt;JavaScript runs, reads &lt;code&gt;localStorage.getItem('theme')&lt;/code&gt;, applies &lt;code&gt;data-theme="dark"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Page re-renders in dark mode&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user sees step 3 -- the light mode render -- before step 4 applies the correct theme. On a fast device, this flash is brief but still jarring. On a slow connection or low-end device, it lasts long enough to be obviously wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Block Rendering Until the Theme Is Applied
&lt;/h2&gt;

&lt;p&gt;The only reliable fix is to apply the theme before the browser makes its first paint. This means running a small inline script in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of the document -- specifically, before any stylesheets have finished loading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- This script runs before any CSS is applied --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;preferred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preferred&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/styles.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a blocking script -- it prevents rendering until it executes. Normally, blocking scripts in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; are a performance anti-pattern because they delay the page from rendering. Here, that blocking behavior is what we need. The script is small enough (under 200 bytes minified) that the blocking cost is negligible, and the tradeoff is correct: a brief delay on all page loads is better than a visible theme flash on every load for users who have set a preference.&lt;/p&gt;

&lt;p&gt;The inline script must be placed before your stylesheet &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags. If the stylesheet loads and paints before the script runs, the flash still occurs.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" rel="noopener noreferrer"&gt;&lt;code&gt;window.matchMedia&lt;/code&gt; API&lt;/a&gt; reads the &lt;code&gt;prefers-color-scheme&lt;/code&gt; media query value from the OS, which is used as the fallback when no stored preference exists in localStorage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Theme Transition on Load
&lt;/h2&gt;

&lt;p&gt;If you have added CSS transitions to smooth theme switching, those same transitions will animate the initial theme application on page load -- which looks like a flash even when the theme is technically applied before first paint. Users see the correct theme animate in from the wrong one.&lt;/p&gt;

&lt;p&gt;The fix is to suppress transitions until after the initial load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Start with transitions disabled */&lt;/span&gt;
&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="nc"&gt;.no-transition&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;preferred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preferred&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-transition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After the DOM is ready, remove the no-transition class&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-transition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;requestAnimationFrame&lt;/code&gt; ensures that the class removal happens after the first frame has been committed, which guarantees transitions are not active during the initial render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-Rendered Applications: Setting the Theme Before HTML Delivery
&lt;/h2&gt;

&lt;p&gt;In a server-rendered application (PHP, Django, Rails, etc.), you can set the &lt;code&gt;data-theme&lt;/code&gt; attribute server-side if the theme preference is stored in a cookie rather than localStorage. The cookie is sent with the request, the server reads it, and the HTML is delivered with &lt;code&gt;data-theme="dark"&lt;/code&gt; already present on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Set-Cookie: theme=dark; SameSite=Strict; Path=/; Max-Age=31536000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server side, read the cookie and render the HTML accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- PHP example --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?php $theme = $_COOKIE['theme'] ?? 'light'; ?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?= htmlspecialchars($theme) ?&amp;gt;"&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates the flash entirely because the HTML arrives from the server already configured with the correct attribute. No client-side JavaScript is required for the initial render.&lt;/p&gt;

&lt;p&gt;The tradeoff: the client-side localStorage approach and the server-side cookie approach are mutually exclusive in their simplest implementations. If you are migrating from localStorage to cookie-based persistence, you need a migration path for existing users whose preferences are in localStorage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js and React Server Components
&lt;/h2&gt;

&lt;p&gt;In Next.js, the standard approach is to use a combination of the &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component with &lt;code&gt;strategy="beforeInteractive"&lt;/code&gt; and a server-side theme resolution via cookies.&lt;/p&gt;

&lt;p&gt;For the client-side fallback, add a &lt;code&gt;beforeInteractive&lt;/code&gt; script in your root &lt;code&gt;layout.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"theme-init"&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"beforeInteractive"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`
            (function() {
              var stored = localStorage.getItem('theme');
              var preferred = stored
                ? stored
                : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
              document.documentElement.setAttribute('data-theme', preferred);
            })();
          `&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;beforeInteractive&lt;/code&gt; strategy renders the script inline in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; before any React hydration occurs, which places it in the same position as the manual inline script approach described above.&lt;/p&gt;

&lt;p&gt;For server-side resolution with cookies in Next.js App Router, read the cookie in the root layout Server Component and set the attribute on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element before delivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;data-theme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This delivers HTML with the correct theme already applied, eliminating the flash entirely for server-rendered pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pure CSS: No Flash by Design
&lt;/h2&gt;

&lt;p&gt;If you use only &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" rel="noopener noreferrer"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" rel="noopener noreferrer"&gt;CSS custom property&lt;/a&gt; overrides without any JavaScript, there is no flash. The CSS media query applies before the page paints, and the correct theme tokens are in place from the first render.&lt;/p&gt;

&lt;p&gt;The limitation is that you cannot offer a manual toggle -- users who want dark mode on a light-OS device, or light mode on a dark-OS device, have no option. For applications where respecting the system default is sufficient, the pure CSS approach is the simplest path with zero flash risk.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The flash-of-wrong-theme bug is one of the first things we look for in a dark mode code review. It is almost always solvable without significant refactoring -- the solution is always some variant of 'run the theme script earlier.' Teams are often surprised that the fix is adding a blocking script, because blocking scripts are normally what you remove in performance optimization work. The blocking here is intentional and the performance cost is negligible." -- Dennis Traina, founder of 137Foundry (&lt;a href="https://137foundry.com/services" rel="noopener noreferrer"&gt;view services&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Verifying Your Fix Works
&lt;/h2&gt;

&lt;p&gt;After implementing one of these approaches, verify the fix with browser DevTools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Chrome DevTools, open the Application tab and clear the site's localStorage.&lt;/li&gt;
&lt;li&gt;Set a &lt;code&gt;theme&lt;/code&gt; value in localStorage (&lt;code&gt;localStorage.setItem('theme', 'dark')&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Reload the page with the DevTools Network tab open and throttling set to "Slow 3G."&lt;/li&gt;
&lt;li&gt;Observe the page load. If a light-mode flash precedes the dark theme, the fix is not in the right position.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A correct implementation shows no visible theme transition on load -- the page arrives in the stored theme without any intermediate state.&lt;/p&gt;

&lt;p&gt;The full dark mode implementation context -- CSS custom properties, the localStorage toggle, and the system preference detection -- is covered in &lt;a href="https://137foundry.com/articles/how-to-add-dark-mode-to-a-web-application" rel="noopener noreferrer"&gt;How to Add Dark Mode to a Web Application&lt;/a&gt;. The FOWT fix described here slots into that broader implementation as the final piece that eliminates the last visible artifact.&lt;/p&gt;

&lt;p&gt;For projects where dark mode is part of a larger front-end architecture scope, &lt;a href="https://137foundry.com/services/web-development" rel="noopener noreferrer"&gt;137Foundry development services&lt;/a&gt; include front-end code review and implementation consulting. FOWT elimination is a consistent item in 137Foundry's front-end review checklist for applications with user-controlled theme switching.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a CSS Design Token System That Scales</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Sun, 31 May 2026 10:39:07 +0000</pubDate>
      <link>https://dev.to/137foundry/building-a-css-design-token-system-that-scales-2nnc</link>
      <guid>https://dev.to/137foundry/building-a-css-design-token-system-that-scales-2nnc</guid>
      <description>&lt;p&gt;A design token system is the layer between your design decisions and your code. It is the difference between a stylesheet where colors, spacing, and border radii are scattered as hard-coded values across hundreds of CSS files, and one where every design decision is named, centralized, and changeable in a single place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" rel="noopener noreferrer"&gt;CSS custom properties&lt;/a&gt; are the native mechanism for implementing design tokens on the web. This article covers how to structure a token system that scales from a small project to a large application, and how to extend it to support dark mode and other theming scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Layers of a Token System
&lt;/h2&gt;

&lt;p&gt;A mature token system has two layers: primitive tokens and semantic tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primitive tokens&lt;/strong&gt; are raw design values with no opinion about where they are used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Color scale */&lt;/span&gt;
  &lt;span class="py"&gt;--blue-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eff6ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--blue-100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#dbeafe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--blue-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b82f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--blue-900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e3a8a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f9fafb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--gray-100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f3f4f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--gray-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6b7280&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111827&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Spacing scale */&lt;/span&gt;
  &lt;span class="py"&gt;--space-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Semantic tokens&lt;/strong&gt; map primitive values to named roles in the UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-text-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-text-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--blue-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--blue-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="py"&gt;--space-component-padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--space-section-gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Components reference semantic tokens exclusively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-accent-hover&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This two-layer structure is what makes the system maintainable at scale. If the brand's primary blue changes from &lt;code&gt;#3b82f6&lt;/code&gt; to &lt;code&gt;#2563eb&lt;/code&gt;, you change one primitive token value. Every semantic token that references &lt;code&gt;--blue-500&lt;/code&gt; updates automatically. Every component that uses &lt;code&gt;--color-accent&lt;/code&gt; updates as a consequence. No grep-and-replace across the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming Tokens by Role, Not by Appearance
&lt;/h2&gt;

&lt;p&gt;The most important decision in token naming is to name by semantic role rather than visual appearance.&lt;/p&gt;

&lt;p&gt;Bad naming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--color-blue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#3&lt;/span&gt;&lt;span class="nt"&gt;b82f6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--color-light-gray&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#f3f4f6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--color-dark-gray&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#1&lt;/span&gt;&lt;span class="nt"&gt;a1a1a&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good naming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--color-accent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#3&lt;/span&gt;&lt;span class="nt"&gt;b82f6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--color-background-secondary&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#f3f4f6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--color-text-primary&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#1&lt;/span&gt;&lt;span class="nt"&gt;a1a1a&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bad naming fails in two ways. First, the name becomes misleading when the value changes -- &lt;code&gt;--color-blue&lt;/code&gt; now holds &lt;code&gt;#2563eb&lt;/code&gt;, which is also blue but the name no longer carries specific information. Second, and more critically, appearance names are meaningless in dark mode. You cannot define &lt;code&gt;--color-light-gray: #0f172a&lt;/code&gt; in a dark theme without the name becoming actively wrong. Semantic names survive theme changes because they describe what the value does, not what it looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the Token System for Dark Mode
&lt;/h2&gt;

&lt;p&gt;Once the semantic token layer is in place, dark mode becomes a single override block. Define the dark mode semantic tokens under a &lt;code&gt;[data-theme="dark"]&lt;/code&gt; attribute selector that JavaScript toggles on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e293b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-text-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-text-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--blue-100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--blue-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every component in the application now responds to the theme change without any component-level changes. The semantic token layer absorbs the entire theming concern.&lt;/p&gt;

&lt;p&gt;You can also use the CSS media feature &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" rel="noopener noreferrer"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt; to apply the dark token set automatically when the user's OS is in dark mode, before any JavaScript runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e293b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--color-text-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--color-text-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--blue-100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[data-theme]&lt;/code&gt; attribute selector, when present in the DOM, overrides the media query result. This gives you the correct priority order: user's explicit preference from the UI toggle takes precedence over the OS default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organizing a Large Token File
&lt;/h2&gt;

&lt;p&gt;For applications with hundreds of tokens, a flat file becomes hard to navigate. Two organizational strategies work well:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layered file structure:&lt;/strong&gt; Split tokens into files by category (&lt;code&gt;tokens/colors.css&lt;/code&gt;, &lt;code&gt;tokens/spacing.css&lt;/code&gt;, &lt;code&gt;tokens/typography.css&lt;/code&gt;) and import them in a single &lt;code&gt;tokens/index.css&lt;/code&gt;. Each file is focused and manageable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment-delimited sections in a single file:&lt;/strong&gt; For smaller projects, sections within one file work well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* =====================
     COLOR - PRIMITIVES
  ===================== */&lt;/span&gt;
  &lt;span class="py"&gt;--blue-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b82f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;

  &lt;span class="c"&gt;/* =====================
     COLOR - SEMANTIC
  ===================== */&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--blue-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;

  &lt;span class="c"&gt;/* =====================
     SPACING
  ===================== */&lt;/span&gt;
  &lt;span class="py"&gt;--space-4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters more than the organizational structure is the convention: every color value used in a component stylesheet must have a corresponding entry in the token file. Any hex value that appears in a component stylesheet without a token reference is a gap in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Add a New Token vs. Use an Existing One
&lt;/h2&gt;

&lt;p&gt;The question of when to add new tokens versus reusing existing ones is where design token systems often accumulate clutter. A useful rule: add a new semantic token only when the UI has a distinct need that does not already have a named representation.&lt;/p&gt;

&lt;p&gt;If a component needs a color that does not fit any existing semantic token, that is a signal that a new semantic category may be needed -- not just a one-off override. If the component is the only place in the application where this distinction matters, consider whether the distinction belongs in the token layer at all or should remain as a component-level style.&lt;/p&gt;

&lt;p&gt;Common token categories that teams consistently underestimate when starting out: interactive state tokens (hover, focus, active, disabled), status tokens (success, warning, error, info), and surface hierarchy tokens (background, surface, overlay, tooltip). Building these categories from the beginning avoids the retrofitting work that comes when the first status message or modal is added and there are no matching tokens in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling That Supports Token System Discipline
&lt;/h2&gt;

&lt;p&gt;Token systems accumulate gaps silently without enforcement. A few tools reduce the maintenance burden:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stylelint with a custom rule&lt;/strong&gt; that flags hard-coded hex values in component files catches gaps at commit time rather than in a periodic audit. A value like &lt;code&gt;background: #3b82f6&lt;/code&gt; in a component file is a lint error; &lt;code&gt;background: var(--color-accent)&lt;/code&gt; is not. This is the highest-leverage automation addition to a token system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS DevTools panel&lt;/strong&gt; in Chrome and Firefox shows computed custom property values per element, which makes debugging theme application problems significantly faster than inspecting computed styles manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual regression snapshots&lt;/strong&gt; of both light and dark mode after any token change catch unintended cascade effects before they reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation: Does Your Token System Cover What It Should?
&lt;/h2&gt;

&lt;p&gt;After implementing a token system, a quick audit catches the most common gaps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grep your stylesheets for hex values (&lt;code&gt;#[0-9a-fA-F]{3,6}&lt;/code&gt;). Any hard-coded hex outside the token file is a missing token.&lt;/li&gt;
&lt;li&gt;Grep for hard-coded pixel values in spacing properties (&lt;code&gt;margin&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, &lt;code&gt;gap&lt;/code&gt;) that do not use &lt;code&gt;var(--space-*)&lt;/code&gt;. These are gaps in the spacing token layer.&lt;/li&gt;
&lt;li&gt;Apply your dark mode toggle and look for elements that remain light. Each one indicates a hard-coded value that bypassed the token system.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"The most common mistake we see in token systems is adding tokens for one-off component needs rather than for genuine semantic distinctions. You end up with fifty color tokens when ten would cover the full design language. Start narrow, expand only when the UI demands it, and keep the token names honest about what they represent." -- Dennis Traina, founder of 137Foundry (&lt;a href="https://137foundry.com/services" rel="noopener noreferrer"&gt;view services&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 137Foundry development team applies this audit at the beginning of any front-end refactor that includes theming work. The grep for hard-coded hex values reliably surfaces a first pass of missing tokens within minutes.&lt;/p&gt;

&lt;p&gt;For the practical implementation of dark mode built on top of a CSS custom property token system, &lt;a href="https://137foundry.com/articles/how-to-add-dark-mode-to-a-web-application" rel="noopener noreferrer"&gt;How to Add Dark Mode to a Web Application&lt;/a&gt; covers the full pattern including localStorage persistence and the flash-of-wrong-theme prevention.&lt;/p&gt;

&lt;p&gt;The browser support for CSS custom properties is comprehensive -- &lt;a href="https://caniuse.com/css-variables" rel="noopener noreferrer"&gt;caniuse confirms coverage&lt;/a&gt; across all modern browsers. This is a technique that is safe to ship without polyfills or build-tool dependencies in any application that does not target Internet Explorer.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Add Fuzzy Search to a JavaScript App With Fuse.js</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Sat, 30 May 2026 10:27:52 +0000</pubDate>
      <link>https://dev.to/137foundry/how-to-add-fuzzy-search-to-a-javascript-app-with-fusejs-cc6</link>
      <guid>https://dev.to/137foundry/how-to-add-fuzzy-search-to-a-javascript-app-with-fusejs-cc6</guid>
      <description>&lt;p&gt;Fuse.js is a zero-dependency JavaScript library that adds fuzzy search to any dataset that fits in memory. It handles typos, partial matches, and relevance ranking without a server or backend service. For datasets under 10,000 small objects, it is one of the fastest ways to add high-quality search to a web application.&lt;/p&gt;

&lt;p&gt;This guide walks through setup, configuration, and the key options that determine how well search results match user intent. For context on the broader search-as-you-type implementation including debounce and request cancellation, the &lt;a href="https://137foundry.com/articles/how-to-build-search-as-you-type-debounce-relevance" rel="noopener noreferrer"&gt;137Foundry guide on search-as-you-type&lt;/a&gt; covers the complete approach. &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; builds custom web applications that incorporate search as a core feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install Fuse.js
&lt;/h2&gt;

&lt;p&gt;Install via npm for module-based projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fuse.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For browser-only projects without a build step, Fuse.js is also available via CDN -- check the &lt;a href="https://github.com/krisk/Fuse/releases" rel="noopener noreferrer"&gt;Fuse.js releases page&lt;/a&gt; for the current version and include it as a script tag pointing to the jsdelivr or unpkg CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Prepare Your Dataset
&lt;/h2&gt;

&lt;p&gt;Fuse.js works best with an array of objects. The library indexes the fields you specify and searches across them. A simple product catalog might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wireless Keyboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Electronics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Compact Bluetooth keyboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USB-C Hub&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Electronics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Multi-port USB hub for laptops&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desk Lamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home Office&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LED lamp with adjustable brightness&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... more items&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataset can come from a JSON file, an API call at page load, or be embedded in the HTML. Fuse.js indexes it in memory; updates to the original array require re-indexing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create the Fuse Instance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fuse&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fuse.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;includeScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Fuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;keys&lt;/code&gt; array tells Fuse which fields to search. The &lt;code&gt;threshold&lt;/code&gt; controls how fuzzy the matching is. A threshold of 0.0 requires exact matches; 1.0 matches everything. A value of 0.3 is a good starting point: it catches typos and minor misspellings without returning too many irrelevant results.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;includeScore: true&lt;/code&gt; adds a relevance score to each result (0.0 is a perfect match, higher values are worse matches). This is useful for debugging and for sorting results that pass the threshold. Enable it during development even if scores are not displayed in the UI -- logging scores for real user queries is the fastest way to determine whether the threshold needs adjustment for your specific dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Search and Render Results
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Each result looks like:&lt;/span&gt;
&lt;span class="c1"&gt;// { item: { id: 1, name: 'Wireless Keyboard', ... }, score: 0.12, refIndex: 0 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fuse.search(query)&lt;/code&gt; returns results sorted by score (best match first). Each result wraps the original item with &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;score&lt;/code&gt;, and &lt;code&gt;refIndex&lt;/code&gt; properties. Access the original object through &lt;code&gt;result.item&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Wire It to a Search Input With Debounce
&lt;/h2&gt;

&lt;p&gt;Combining Fuse.js with debounce gives you instant, typo-tolerant search without any server requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultsContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;renderResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;searchInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;handleSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resultsContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;No results found.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;resultsContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;div class="result"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Fuse.js searches in memory, there are no race conditions and no AbortController needed. The debounce delay can be shorter (150-200ms) than a server-based search because the search itself is instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Field Weights
&lt;/h2&gt;

&lt;p&gt;Not all fields are equally important. Matching a query in the product name is more relevant than matching it in the description. Fuse.js supports per-key weights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher weight values mean matches in that field boost the result's score more. A product whose name matches the query will rank significantly above one where only the description matches.&lt;/p&gt;

&lt;p&gt;Getting weights right usually takes experimentation. Start with name weighted 3x description and adjust based on what kinds of queries your users actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Nested Objects
&lt;/h2&gt;

&lt;p&gt;Fuse.js can search nested fields using dot notation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Article A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author.name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arrays of strings within an object are also supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wireless&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bluetooth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fuse.js searches each element of the array and treats a match in any element as a match for that field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limiting the Result Set
&lt;/h2&gt;

&lt;p&gt;By default, Fuse.js returns all results above the threshold. For a search UI, this can be too many. Limit results with the &lt;code&gt;limit&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fuse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or slice the results array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Limiting to 10-20 results is appropriate for most search UIs. Showing more than that without pagination creates a list that users do not scroll through anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Fuse.js Is Not the Right Tool
&lt;/h2&gt;

&lt;p&gt;Fuse.js has limits. It does not support full-text search features like stemming (treating "running" and "runs" as the same term), phrase search, or proximity search. It loads the full dataset into memory, which is fine for small datasets but impractical for large ones. Memory use scales with object count and field size: ten thousand small objects might use 3-4 MB of browser memory, while one hundred thousand records with longer text fields can push past 20 MB and cause noticeable slowdowns on lower-end mobile hardware.&lt;/p&gt;

&lt;p&gt;For datasets over 10,000 items, consider &lt;a href="https://lunrjs.com" rel="noopener noreferrer"&gt;Lunr.js&lt;/a&gt; (pre-built inverted index), a server-side search API, or a dedicated search service like &lt;a href="https://www.algolia.com" rel="noopener noreferrer"&gt;Algolia&lt;/a&gt; or &lt;a href="https://typesense.org" rel="noopener noreferrer"&gt;Typesense&lt;/a&gt;. For the backend implementation and the server-side patterns that apply when the dataset is too large for client-side search, the &lt;a href="https://137foundry.com/articles/how-to-build-search-as-you-type-debounce-relevance" rel="noopener noreferrer"&gt;137Foundry guide on search-as-you-type&lt;/a&gt; covers the server-side approach including &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" rel="noopener noreferrer"&gt;AbortController&lt;/a&gt; for managing in-flight requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Complete Minimal Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fuse&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fuse.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wireless Keyboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Electronics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USB Hub&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Electronics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Monitor Stand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Furniture&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Fuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;includeScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;div&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is production-ready for small datasets. Add weight tuning, empty state messaging, and ARIA attributes for the complete implementation. &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; handles the full stack when search is a core requirement, from library selection through relevance tuning and ongoing optimization.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>search</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>7 Free Search Libraries and Tools for JavaScript Web Apps</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Sat, 30 May 2026 10:25:32 +0000</pubDate>
      <link>https://dev.to/137foundry/7-free-search-libraries-and-tools-for-javascript-web-apps-30n5</link>
      <guid>https://dev.to/137foundry/7-free-search-libraries-and-tools-for-javascript-web-apps-30n5</guid>
      <description>&lt;p&gt;Building search into a web application means choosing between client-side and server-side approaches, and between writing everything yourself or using a library that handles the heavy lifting. These seven tools cover the range from simple fuzzy matching to full production search engines.&lt;/p&gt;

&lt;p&gt;For the implementation patterns that work regardless of which tool you choose, including debounce, request cancellation, and relevance scoring, the &lt;a href="https://137foundry.com/articles/how-to-build-search-as-you-type-debounce-relevance" rel="noopener noreferrer"&gt;137Foundry guide on search-as-you-type&lt;/a&gt; covers the complete approach. &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; integrates these libraries into production web applications where search is a core feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Fuse.js
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fusejs.io" rel="noopener noreferrer"&gt;Fuse.js&lt;/a&gt;&lt;/strong&gt; is a lightweight, zero-dependency JavaScript library for fuzzy searching. It works entirely in the browser, which means no server round-trip and no backend configuration. You pass it an array of objects and a set of keys to search, and it returns ranked results with fuzzy matching that handles typos.&lt;/p&gt;

&lt;p&gt;Fuse.js is the right choice when your dataset fits in memory (typically under 10,000 small objects), when you want instant results without server requests, and when the full dataset can be safely exposed to the client. It supports field weighting, exact match priority over fuzzy matches, and a configurable fuzzy threshold.&lt;/p&gt;

&lt;p&gt;Use it for: site-wide content search, documentation search, product catalog search for small stores, command palette interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Lunr.js
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://lunrjs.com" rel="noopener noreferrer"&gt;Lunr.js&lt;/a&gt;&lt;/strong&gt; is a full-text search library for JavaScript, designed to create search indexes that behave like Solr or Elasticsearch but run entirely in the browser. Unlike Fuse.js, Lunr pre-builds an inverted index from your dataset, which makes large-dataset search significantly faster at the cost of an upfront indexing step.&lt;/p&gt;

&lt;p&gt;Lunr supports boosting (weighting specific fields), stop words, stemming, and wildcard searches. The index can be built server-side and serialized to JSON for faster client-side loading. It is a good step up from Fuse.js when the dataset is larger or when you need proper full-text ranking rather than fuzzy matching.&lt;/p&gt;

&lt;p&gt;Use it for: documentation sites, offline-capable web apps, static site search, any use case where the dataset changes infrequently.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Typesense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://typesense.org" rel="noopener noreferrer"&gt;Typesense&lt;/a&gt;&lt;/strong&gt; is an open-source search engine you can self-host or use through Typesense Cloud. It offers sub-10ms search responses, typo tolerance, faceting, and a clean API. Unlike a general-purpose database with full-text search bolted on, Typesense is designed specifically for instant search.&lt;/p&gt;

&lt;p&gt;Typesense has official JavaScript and Node.js clients, React and Vue integrations, and a well-documented instant search UI widget library. For teams that want search-engine performance without paying for a SaaS product, self-hosting Typesense on a small instance is a viable option.&lt;/p&gt;

&lt;p&gt;Use it for: e-commerce search, SaaS application search, any product where search quality is a competitive feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Meilisearch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.meilisearch.com" rel="noopener noreferrer"&gt;Meilisearch&lt;/a&gt;&lt;/strong&gt; is another open-source, self-hostable search engine with instant search results, typo tolerance, and faceting. Its developer experience is particularly smooth: simple HTTP API, official SDKs for most languages, and a dashboard for exploring your search index. Meilisearch is designed to be easy to set up and run, which makes it a popular choice for development teams that want search-engine quality without extensive DevOps work.&lt;/p&gt;

&lt;p&gt;Meilisearch has a cloud hosted offering for teams that prefer not to manage infrastructure.&lt;/p&gt;

&lt;p&gt;Use it for: the same use cases as Typesense; the choice between the two usually comes down to API preference and deployment environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Algolia
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.algolia.com" rel="noopener noreferrer"&gt;Algolia&lt;/a&gt;&lt;/strong&gt; is the industry-standard hosted search-as-a-service platform. It handles infrastructure, relevance tuning, and scale; you send it your data and query it through their API. Algolia's InstantSearch library provides pre-built UI components for search inputs, results, facets, and pagination that work with React, Vue, Angular, and vanilla JavaScript.&lt;/p&gt;

&lt;p&gt;Algolia is not free at scale, but it has a generous free tier suitable for small projects. Its primary advantages over self-hosted options are zero infrastructure management, extremely fast global CDN-backed responses, and a mature relevance tuning UI.&lt;/p&gt;

&lt;p&gt;Use it for: production applications where search quality and response time matter, teams without dedicated DevOps capacity, global applications that need low-latency search across regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. FlexSearch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/nextapps-de/flexsearch" rel="noopener noreferrer"&gt;FlexSearch&lt;/a&gt;&lt;/strong&gt; is a high-performance full-text search library for JavaScript that claims to be the fastest in-browser search library available. It uses a different indexing approach than Lunr.js, trading some of Lunr's feature richness for raw speed. It supports multiple scoring algorithms, field weighting, and incremental updates to the index.&lt;/p&gt;

&lt;p&gt;FlexSearch is a strong option when you have a large client-side dataset and search response time is the primary concern. It supports both browser and Node.js environments.&lt;/p&gt;

&lt;p&gt;Use it for: large client-side datasets where Fuse.js starts to feel slow, real-time search in rich JavaScript applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. PostgreSQL Full-Text Search
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.postgresql.org/docs/current/textsearch.html" rel="noopener noreferrer"&gt;PostgreSQL's built-in full-text search&lt;/a&gt;&lt;/strong&gt; is not a separate library, but it is frequently overlooked as a serious search option. Using &lt;code&gt;tsvector&lt;/code&gt; and &lt;code&gt;tsquery&lt;/code&gt;, PostgreSQL supports ranked full-text search with stemming, phrase search, proximity search, and field weighting.&lt;/p&gt;

&lt;p&gt;For applications already using PostgreSQL, adding full-text search indexes avoids the operational overhead of a separate search service. It scales to millions of records with proper indexing and handles most search use cases without dedicated search infrastructure.&lt;/p&gt;

&lt;p&gt;The limitation is that PostgreSQL full-text search does not support typo tolerance out of the box (fuzzy matching requires the &lt;code&gt;pg_trgm&lt;/code&gt; extension) and its ranking algorithm is not as sophisticated as Algolia or Typesense. For many applications, though, it is plenty.&lt;/p&gt;

&lt;p&gt;Use it for: applications already on PostgreSQL where search needs are moderate, teams that prefer fewer infrastructure components, internal tools and admin interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Between Them
&lt;/h2&gt;

&lt;p&gt;The decision usually breaks down along two dimensions: whether the data can be loaded client-side or needs to stay on the server, and whether the team can manage a separate search infrastructure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client-side, small dataset, zero config&lt;/td&gt;
&lt;td&gt;Fuse.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client-side, larger dataset, full-text ranking&lt;/td&gt;
&lt;td&gt;Lunr.js or FlexSearch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side, self-hosted, free&lt;/td&gt;
&lt;td&gt;Typesense or Meilisearch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side, hosted service&lt;/td&gt;
&lt;td&gt;Algolia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side, existing PostgreSQL&lt;/td&gt;
&lt;td&gt;pg full-text + pg_trgm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Questions to Ask Before Choosing
&lt;/h2&gt;

&lt;p&gt;Before committing to a library, three questions narrow the options quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the full dataset be safely exposed to the client?&lt;/strong&gt; Client-side libraries (Fuse.js, Lunr.js, FlexSearch) load the dataset into the browser. If the data contains prices, user records, unpublished content, or anything that should not be visible to all end users, client-side search is not appropriate regardless of performance benefits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often does the dataset change?&lt;/strong&gt; Fuse.js re-indexes on each page load, which works when data changes infrequently. If records are added, updated, or deleted constantly, a server-side index with real-time update hooks handles freshness more reliably than reloading a static JSON file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What level of relevance quality is required?&lt;/strong&gt; For internal tools and admin interfaces, basic keyword matching is usually sufficient. For customer-facing product search, features like typo tolerance, field weighting, and faceting make a visible difference in how often users find what they are looking for on the first query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the team's operational capacity?&lt;/strong&gt; Algolia requires no infrastructure management but costs money at scale. Typesense and Meilisearch require a server but are free and self-hosted. PostgreSQL full-text requires no additional infrastructure if you are already running PostgreSQL.&lt;/p&gt;

&lt;p&gt;The front-end implementation patterns (debounce, AbortController, loading states) apply regardless of which backend you use. The &lt;a href="https://137foundry.com/articles/how-to-build-search-as-you-type-debounce-relevance" rel="noopener noreferrer"&gt;guide on building search-as-you-type&lt;/a&gt; covers those patterns in detail. &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt; has integrated Algolia, Typesense, and PostgreSQL full-text search into client projects across different scale requirements.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>search</category>
      <category>tools</category>
    </item>
    <item>
      <title>Python Try-Except vs Try-Finally: When to Use Each</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Fri, 29 May 2026 11:49:30 +0000</pubDate>
      <link>https://dev.to/137foundry/python-try-except-vs-try-finally-when-to-use-each-15h1</link>
      <guid>https://dev.to/137foundry/python-try-except-vs-try-finally-when-to-use-each-15h1</guid>
      <description>&lt;p&gt;Both &lt;code&gt;try-except&lt;/code&gt; and &lt;code&gt;try-finally&lt;/code&gt; are exception handling constructs in Python, but they serve different purposes. Mixing them up produces code that works accidentally rather than by design. This guide covers when each pattern is appropriate, when to combine them, and the specific cases where the distinction matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Each Construct Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;try-except&lt;/code&gt; catches exceptions. When an exception occurs inside the &lt;code&gt;try&lt;/code&gt; block, Python looks for an &lt;code&gt;except&lt;/code&gt; clause that matches the exception type and executes it instead of propagating the exception.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;try-finally&lt;/code&gt; guarantees cleanup. The &lt;code&gt;finally&lt;/code&gt; block runs no matter what happens in the &lt;code&gt;try&lt;/code&gt; block -- whether the code completes normally, raises an exception, executes a &lt;code&gt;return&lt;/code&gt; statement, or even calls &lt;code&gt;sys.exit()&lt;/code&gt;. Exceptions are not caught or suppressed; they continue propagating after &lt;code&gt;finally&lt;/code&gt; runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# try-except: handles the exception
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;# Exception caught; execution continues here
&lt;/span&gt;
&lt;span class="c1"&gt;# try-finally: guarantees cleanup regardless of outcome
&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Always runs, even if query raised
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Use try-except
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;try-except&lt;/code&gt; when you have a specific plan for what to do if an exception occurs. "A specific plan" means one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return a fallback value&lt;/li&gt;
&lt;li&gt;Log the error and continue with the next item&lt;/li&gt;
&lt;li&gt;Convert the exception to a different type (exception chaining)&lt;/li&gt;
&lt;li&gt;Re-raise with additional context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical distinction is catching &lt;em&gt;specific&lt;/em&gt; exception types, not &lt;code&gt;Exception&lt;/code&gt; or bare &lt;code&gt;except:&lt;/code&gt;. Broad catches hide unexpected errors and make debugging harder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Good: specific exception type, specific handling
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;UserNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;DatabaseConnectionError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ServiceUnavailableError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Database offline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;

&lt;span class="c1"&gt;# Problematic: catches everything including programming errors
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;process_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Silently discards AttributeErrors, NameErrors, etc.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.python.org" rel="noopener noreferrer"&gt;Python language documentation&lt;/a&gt; covers the full built-in exception hierarchy and which errors should be caught versus propagated.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use try-finally
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;try-finally&lt;/code&gt; when you need to release a resource or restore state, regardless of whether the operation succeeded. The resource does not have to be a file or database connection -- it could be a lock, a temporary directory, a global flag, or any state that must be cleaned up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_with_temp_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mktemp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block here runs whether &lt;code&gt;transform&lt;/code&gt; succeeds, raises, or even if the function is interrupted. The temp file is always cleaned up.&lt;/p&gt;

&lt;p&gt;For resources that implement the context manager protocol -- files, database connections from most ORMs, locks -- a &lt;code&gt;with&lt;/code&gt; statement is cleaner and equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# File is closed here even if read() raised
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit &lt;code&gt;try-finally&lt;/code&gt; pattern remains appropriate when you need conditional cleanup, when the resource object is not a context manager, or when multiple resources need to be managed with different cleanup logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining try-except-else-finally
&lt;/h2&gt;

&lt;p&gt;Python allows all four clauses together. The &lt;code&gt;else&lt;/code&gt; clause adds a fourth behavior: code that runs only when no exception was raised.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;DatabaseConnectionError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB connection failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;QueryError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query failed: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Only runs if try completed without raising
&lt;/span&gt;    &lt;span class="nf"&gt;process_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query returned %d rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Always runs
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;else&lt;/code&gt; clause is the most underused part of this pattern. Without it, success-path code goes inside &lt;code&gt;try&lt;/code&gt;, which means a &lt;code&gt;QueryError&lt;/code&gt; raised by &lt;code&gt;process_rows&lt;/code&gt; would be caught by the &lt;code&gt;except QueryError&lt;/code&gt; handler -- almost certainly not the intended behavior. Putting success-path logic in &lt;code&gt;else&lt;/code&gt; limits the &lt;code&gt;except&lt;/code&gt; clauses to errors that actually originate in the &lt;code&gt;try&lt;/code&gt; block.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Mistake: try-except as a Substitute for finally
&lt;/h2&gt;

&lt;p&gt;A common mistake is using &lt;code&gt;try-except&lt;/code&gt; to handle cleanup, under the assumption that catching exceptions covers all cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong: cleanup only happens if exception occurs
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# This is cleanup, not exception handling
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;span class="c1"&gt;# connection is never closed if query succeeds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This closes the connection if an exception occurs but not if the query succeeds. The correct structure uses &lt;code&gt;finally&lt;/code&gt; for cleanup, with &lt;code&gt;except&lt;/code&gt; only for cases where the caller has a specific response planned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;QueryError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query failed: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Always runs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Separate Concerns Into Separate try Blocks
&lt;/h2&gt;

&lt;p&gt;When you need different handling for different operations, separate them into different &lt;code&gt;try&lt;/code&gt; blocks rather than catching multiple exception types in one broad handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Single try block: hard to tell which operation failed
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;send_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Something failed: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Separated: each operation has its own handler
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;UserNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;APIError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Profile fetch failed for %s: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nf"&gt;send_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separated version makes the intent explicit at each point and avoids the ambiguity of which operation the exception came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Both Paths
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pytest.org" rel="noopener noreferrer"&gt;pytest documentation at docs.pytest.org&lt;/a&gt; covers &lt;code&gt;pytest.raises&lt;/code&gt; for asserting that exceptions are raised and &lt;code&gt;mocker.patch&lt;/code&gt; for simulating failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_cleanup_runs_on_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mocker&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mock_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db.connect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mock_conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;side_effect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QueryError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QueryError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mock_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Verify finally ran
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing that cleanup runs on failure confirms the &lt;code&gt;finally&lt;/code&gt; block is doing its job. Without this test, a refactor that accidentally moves the &lt;code&gt;close()&lt;/code&gt; call inside &lt;code&gt;else&lt;/code&gt; would not be caught until production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Managers as a Formalized try-finally
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;with&lt;/code&gt; statement is Python's built-in way to encapsulate &lt;code&gt;try-finally&lt;/code&gt; cleanup into a reusable object. When you open a file with &lt;code&gt;with open(path) as f:&lt;/code&gt;, the file object's &lt;code&gt;__enter__&lt;/code&gt; method runs at entry and &lt;code&gt;__exit__&lt;/code&gt; method runs at exit, regardless of whether an exception occurred. It is precisely equivalent to wrapping the block in &lt;code&gt;try-finally&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can define your own context managers using &lt;code&gt;contextlib.contextmanager&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;

&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;managed_connection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;managed_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;yield&lt;/code&gt; splits the function into the setup (before yield) and teardown (after yield) phases. The &lt;code&gt;finally&lt;/code&gt; in the context manager function guarantees the teardown runs. This is a cleaner way to express reusable resource management than repeating &lt;code&gt;try-finally&lt;/code&gt; blocks throughout the codebase.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.python.org" rel="noopener noreferrer"&gt;Python documentation at docs.python.org&lt;/a&gt; covers the context manager protocol (&lt;code&gt;__enter__&lt;/code&gt; and &lt;code&gt;__exit__&lt;/code&gt;) and the &lt;code&gt;contextlib&lt;/code&gt; module in depth. The &lt;code&gt;contextlib.suppress&lt;/code&gt; function is also part of &lt;code&gt;contextlib&lt;/code&gt; and provides a concise alternative to &lt;code&gt;try-except: pass&lt;/code&gt; for cases where an exception is genuinely expected and should be silenced cleanly. &lt;a href="https://peps.python.org" rel="noopener noreferrer"&gt;Python Enhancement Proposal 343 on peps.python.org&lt;/a&gt; introduced the &lt;code&gt;with&lt;/code&gt; statement and explains the design intent behind the context manager protocol -- the &lt;code&gt;__enter__&lt;/code&gt; / &lt;code&gt;__exit__&lt;/code&gt; approach was chosen specifically to guarantee cleanup even in the presence of exceptions, &lt;code&gt;return&lt;/code&gt; statements, and generator-based control flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;The full collection of Python exception handling patterns -- including exception chaining, &lt;code&gt;contextlib.suppress&lt;/code&gt;, logging with &lt;code&gt;log.exception()&lt;/code&gt;, and custom exception hierarchies -- is in the &lt;a href="https://137foundry.com/articles/python-error-handling-code-snippets-try-except-patterns" rel="noopener noreferrer"&gt;Python Error Handling article on the 137Foundry blog&lt;/a&gt;. The &lt;code&gt;try-except-else-finally&lt;/code&gt; pattern is covered there with additional context on how it fits into service boundary design.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Python Custom Exception Classes: When and How to Define Your Own</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Fri, 29 May 2026 11:49:29 +0000</pubDate>
      <link>https://dev.to/137foundry/python-custom-exception-classes-when-and-how-to-define-your-own-17m3</link>
      <guid>https://dev.to/137foundry/python-custom-exception-classes-when-and-how-to-define-your-own-17m3</guid>
      <description>&lt;p&gt;The built-in Python exception hierarchy covers system errors, input/output failures, and programming mistakes. It does not cover the domain-specific failure modes of your application. A &lt;code&gt;UserNotFoundError&lt;/code&gt; is not in the standard library. Neither is &lt;code&gt;RetryableError&lt;/code&gt;, &lt;code&gt;InsufficientFundsError&lt;/code&gt;, or &lt;code&gt;DataCorruptionError&lt;/code&gt;. Those are yours to define, and defining them correctly is the difference between exception handling that communicates intent and exception handling that just prevents crashes.&lt;/p&gt;

&lt;p&gt;This post covers the practical side: when to create custom exceptions, how to structure them, what to put in them, and the common mistakes worth avoiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Built-In Exceptions Are Not Enough
&lt;/h2&gt;

&lt;p&gt;Built-in exceptions work well for errors that are universal and well-understood: &lt;code&gt;ValueError&lt;/code&gt; for bad input, &lt;code&gt;KeyError&lt;/code&gt; for missing dictionary keys, &lt;code&gt;FileNotFoundError&lt;/code&gt; for missing paths. They fail as a communication mechanism when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The caller needs to distinguish between failure modes.&lt;/strong&gt; If your service can fail with a temporary network problem or a permanent data validation error, the caller needs to handle these differently. Raising &lt;code&gt;Exception("temporary")&lt;/code&gt; or &lt;code&gt;Exception("permanent")&lt;/code&gt; makes that distinction fragile -- it depends on string matching, not type matching.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The failure carries structured data.&lt;/strong&gt; A &lt;code&gt;DataProcessingError&lt;/code&gt; that includes the record ID, the source file, and the failed field name is far more actionable than a plain string message. Custom exception classes let you attach that data as attributes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to catch all failures from a subsystem without catching everything.&lt;/strong&gt; A background job processor can catch &lt;code&gt;ServiceError&lt;/code&gt; to handle any failure from the service layer while still letting &lt;code&gt;MemoryError&lt;/code&gt; or &lt;code&gt;KeyboardInterrupt&lt;/code&gt; propagate.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Minimal Custom Exception
&lt;/h2&gt;

&lt;p&gt;The minimal correct custom exception is three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserNotFoundError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Raised when a user lookup returns no result.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all you need to make &lt;code&gt;UserNotFoundError&lt;/code&gt; a distinct type that callers can catch specifically. The docstring is the class documentation, not an error message.&lt;/p&gt;

&lt;p&gt;You do not need to define &lt;code&gt;__init__&lt;/code&gt; unless you need custom attributes. The default &lt;code&gt;Exception.__init__&lt;/code&gt; accepts a message string and stores it as &lt;code&gt;self.args[0]&lt;/code&gt;, which is what prints when the exception is displayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Structured Data to Custom Exceptions
&lt;/h2&gt;

&lt;p&gt;When the exception needs to carry context beyond a message, define &lt;code&gt;__init__&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataProcessingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record_id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;super().__init__(message)&lt;/code&gt; to preserve standard exception behavior -- the message will appear in tracebacks and &lt;code&gt;str(err)&lt;/code&gt; will return it. Custom attributes go after.&lt;/p&gt;

&lt;p&gt;Usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;DataProcessingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid date format in record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller can now access &lt;code&gt;err.record_id&lt;/code&gt; and &lt;code&gt;err.field&lt;/code&gt; to build a specific error response or retry payload, rather than parsing the message string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Hierarchy
&lt;/h2&gt;

&lt;p&gt;A flat list of custom exceptions works for small codebases. At scale, a hierarchy rooted at a custom base class lets callers choose how broadly to catch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Base for all service-layer failures.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetryableError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The operation failed but may succeed on retry.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PermanentError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The operation will never succeed on retry.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermanentError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The requested resource does not exist.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource_id&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource_type&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A background job processor can now catch &lt;code&gt;RetryableError&lt;/code&gt; to schedule retry, catch &lt;code&gt;PermanentError&lt;/code&gt; to escalate and discard, and let unexpected exceptions propagate to the outer error handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RetryableError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;PermanentError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Permanent failure on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_failed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exception Chaining for Cause Preservation
&lt;/h2&gt;

&lt;p&gt;When you catch a low-level exception and raise a domain-level one, use &lt;code&gt;raise X from Y&lt;/code&gt; to preserve the original cause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;DatabaseConnectionError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RetryableError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Database temporarily unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;from err&lt;/code&gt; clause sets &lt;code&gt;__cause__&lt;/code&gt; on the new exception. When the traceback prints, Python shows both the original &lt;code&gt;DatabaseConnectionError&lt;/code&gt; and the new &lt;code&gt;RetryableError&lt;/code&gt;. The root cause is not replaced -- it is chained. The &lt;a href="https://peps.python.org" rel="noopener noreferrer"&gt;Python Enhancement Proposals on peps.python.org&lt;/a&gt; -- specifically PEP 3134 -- explain the design intent behind &lt;code&gt;__cause__&lt;/code&gt; and &lt;code&gt;__context__&lt;/code&gt;. The &lt;a href="https://docs.python.org" rel="noopener noreferrer"&gt;Python language documentation&lt;/a&gt; covers the full exception hierarchy and the chaining semantics.&lt;/p&gt;

&lt;p&gt;Without chaining, the database error disappears from the traceback. The operator sees a &lt;code&gt;RetryableError&lt;/code&gt; with no indication of what the underlying problem was, which makes root cause analysis much harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Inheriting from BaseException instead of Exception.&lt;/strong&gt; &lt;code&gt;BaseException&lt;/code&gt; is the root of all exceptions including &lt;code&gt;SystemExit&lt;/code&gt; and &lt;code&gt;KeyboardInterrupt&lt;/code&gt;. Your custom exception should inherit from &lt;code&gt;Exception&lt;/code&gt; (or a subclass), not from &lt;code&gt;BaseException&lt;/code&gt;. Inheriting from &lt;code&gt;BaseException&lt;/code&gt; means your exception bypasses broad &lt;code&gt;except Exception&lt;/code&gt; handlers that are catching for cleanup purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catching your own base class too broadly in the service.&lt;/strong&gt; A service that catches &lt;code&gt;ServiceError&lt;/code&gt; everywhere to suppress failures is using the exception hierarchy for suppression rather than discrimination. Catch at the appropriate level: the outer error boundary handles &lt;code&gt;ServiceError&lt;/code&gt; broadly; internal logic handles &lt;code&gt;RetryableError&lt;/code&gt; and &lt;code&gt;PermanentError&lt;/code&gt; specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Putting too much in the message, not enough in attributes.&lt;/strong&gt; An error message that reads &lt;code&gt;"Record 4823 failed on field 'amount' with value '$14.00'"&lt;/code&gt; carries useful data in an inaccessible form. The record ID, field name, and value should be separate attributes so callers can route, log, or persist them without parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting to call super().&lt;strong&gt;init&lt;/strong&gt;().&lt;/strong&gt; If you define &lt;code&gt;__init__&lt;/code&gt; without calling &lt;code&gt;super().__init__(message)&lt;/code&gt;, the exception message will not appear in the traceback or in &lt;code&gt;str(err)&lt;/code&gt;. This is a common source of confusing traceback output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Custom Exceptions
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pytest.org" rel="noopener noreferrer"&gt;pytest documentation at docs.pytest.org&lt;/a&gt; covers &lt;code&gt;pytest.raises&lt;/code&gt; with the &lt;code&gt;match&lt;/code&gt; parameter for asserting on exception messages, and accessing &lt;code&gt;exc_info.value&lt;/code&gt; for asserting on custom attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_not_found_error_carries_resource_info&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonexistent_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nonexistent_id&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_retryable_error_on_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mocker&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service.db.query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;side_effect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RetryableError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing that the retry logic triggers on &lt;code&gt;RetryableError&lt;/code&gt; is only meaningful if the test also verifies that the right exception type is raised on timeout. Custom exception hierarchies make this testing pattern natural.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlling How Custom Exceptions Display
&lt;/h2&gt;

&lt;p&gt;By default, an exception displays its class name and the message passed to &lt;code&gt;__init__&lt;/code&gt;. Sometimes you want a different string representation -- for example, when the exception stores multiple attributes and you want the display to include all of them.&lt;/p&gt;

&lt;p&gt;Override &lt;code&gt;__str__&lt;/code&gt; to customize the display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;must be positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "amount: must be positive (got '-5')"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;super().__init__(message)&lt;/code&gt; with the formatted message ensures the display is correct in tracebacks, in &lt;code&gt;str(err)&lt;/code&gt;, and in logging. This is important: if you define &lt;code&gt;__init__&lt;/code&gt; without calling &lt;code&gt;super().__init__()&lt;/code&gt;, the exception will display as &lt;code&gt;ValidationError()&lt;/code&gt; with no message, which is confusing in tracebacks.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;!r&lt;/code&gt; formatting in f-strings applies &lt;code&gt;repr()&lt;/code&gt; to the value, which adds quotes around strings and shows the type clearly. This is useful in exception messages where the value might be an empty string, &lt;code&gt;None&lt;/code&gt;, or a type that looks similar to another at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;The full reference for Python exception handling patterns -- including &lt;code&gt;try-except-else-finally&lt;/code&gt;, &lt;code&gt;contextlib.suppress&lt;/code&gt;, logging with &lt;code&gt;log.exception()&lt;/code&gt;, and re-raising with bare &lt;code&gt;raise&lt;/code&gt; -- is collected in the &lt;a href="https://137foundry.com/articles/python-error-handling-code-snippets-try-except-patterns" rel="noopener noreferrer"&gt;Python Error Handling article on the 137Foundry blog&lt;/a&gt;. Custom exception classes are most effective when the rest of the exception handling stack is also well-structured: a hierarchy without consistent catch/raise patterns at service boundaries will not deliver its full diagnostic value.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Python Libraries for Building Message Queue Consumers</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Thu, 28 May 2026 10:27:59 +0000</pubDate>
      <link>https://dev.to/137foundry/python-libraries-for-building-message-queue-consumers-1kcf</link>
      <guid>https://dev.to/137foundry/python-libraries-for-building-message-queue-consumers-1kcf</guid>
      <description>&lt;p&gt;Building an event-driven data pipeline in Python requires choosing a library that fits your broker, your processing model, and your operational requirements. Here are the primary options, with a plain-language breakdown of what each is best for.&lt;/p&gt;

&lt;p&gt;The right choice usually comes down to three factors: which broker you're already running (or can support operationally), how much task management abstraction you need, and whether your processing model is synchronous or async. Getting this wrong early means retrofitting later -- either migrating the library mid-project or working around limitations in retry, dead-letter queue, or monitoring support.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Celery
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/celery/" rel="noopener noreferrer"&gt;https://pypi.org/project/celery/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Celery is the most widely used Python distributed task queue. It runs on top of Redis or RabbitMQ and handles the full worker lifecycle: task distribution, retry logic, rate limiting, result storage, scheduling, and monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; Celery abstracts most of the message broker complexity. You define tasks as Python functions, decorate them with &lt;code&gt;@app.task&lt;/code&gt;, and Celery handles distributing them to workers. Retry configuration, countdown timers, and rate limits are built-in. The task model is clean and familiar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational overhead:&lt;/strong&gt; Celery runs worker processes that need to be managed separately from your application. The full feature set (Celery Beat for scheduling, Flower for monitoring) adds operational components that need to be deployed and maintained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Python applications that need reliable distributed task processing with built-in retry, rate limiting, and monitoring. The standard choice for production data pipelines that need more than a raw queue client.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. RQ (Redis Queue)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/rq/" rel="noopener noreferrer"&gt;https://pypi.org/project/rq/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;RQ is a simpler Python task queue that runs exclusively on &lt;a href="https://redis.io" rel="noopener noreferrer"&gt;Redis&lt;/a&gt;. It trades Celery's feature richness for a significantly simpler setup and mental model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; RQ is easy to understand and operate. Enqueue a function with &lt;code&gt;q.enqueue(process_event, event_data)&lt;/code&gt;. Run workers with &lt;code&gt;rq worker&lt;/code&gt;. The dashboard (rq-dashboard) is lightweight and informative. For teams that find Celery's configuration overwhelming, RQ is often the right starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational overhead:&lt;/strong&gt; Lower than Celery. Fewer moving parts. The reduced feature set means less to configure and less to break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams that want a simple, Redis-based task queue without the full Celery feature set. Good for smaller pipelines or as a learning entry point before scaling to Celery.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Dramatiq
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/dramatiq/" rel="noopener noreferrer"&gt;https://pypi.org/project/dramatiq/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dramatiq is a newer Python task processing library that runs on Redis or &lt;a href="https://www.rabbitmq.com" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;. It's designed to be more predictable than Celery, with simpler configuration and better default retry behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; Dramatiq's retry behavior is more opinionated and consistent than Celery's out of the box. The actor model is clean: define a function with &lt;code&gt;@dramatiq.actor&lt;/code&gt; and send it messages. Error handling and middleware are composable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational overhead:&lt;/strong&gt; Similar to Celery but with simpler defaults. Fewer gotchas around serialization and worker configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams that have been burned by Celery's configuration complexity but need more features than RQ provides. A reasonable middle ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. redis-py Streams API
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/redis/" rel="noopener noreferrer"&gt;https://pypi.org/project/redis/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;redis-py&lt;/code&gt; library, &lt;a href="https://redis.io" rel="noopener noreferrer"&gt;Redis&lt;/a&gt;'s official Python client, includes a full Streams API for working with Redis Streams natively. Redis Streams are a persistent, log-based message primitive that supports consumer groups, message acknowledgment, and pending entry lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; Direct access to Redis Streams without a task framework layer. Full control over consumer group setup, message acknowledgment, and pending entry management. Lower latency than Celery for simple consumer patterns because there's no framework overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational overhead:&lt;/strong&gt; You write the consumer loop yourself. Retry logic, dead-letter queue behavior, and worker management all need to be implemented in application code. This is lower-level than Celery or RQ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Pipelines that need precise control over message delivery semantics and don't need Celery's task management features. Also useful for teams that want to understand message queue mechanics before adopting a higher-level framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. aio-pika
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/aio-pika/" rel="noopener noreferrer"&gt;https://pypi.org/project/aio-pika/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;aio-pika is an asyncio-native Python client for RabbitMQ. Unlike Celery (which uses synchronous workers by default) or redis-py's synchronous queue commands, aio-pika is designed for async consumer code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; Full AMQP support with asyncio integration. Suitable for pipelines where the consumer logic includes async I/O (database calls, API requests, file operations) and you want to take advantage of Python's asyncio concurrency rather than spawning multiple synchronous workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational overhead:&lt;/strong&gt; Requires asyncio proficiency. Not a drop-in replacement for Celery or RQ -- it's a different model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; High-throughput pipelines where the processing involves I/O-bound async operations and you're building on an asyncio-based Python application. Not the default choice for CPU-bound processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. pika
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/pika/" rel="noopener noreferrer"&gt;https://pypi.org/project/pika/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pika is &lt;a href="https://www.rabbitmq.com" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;'s official Python client library. It provides low-level AMQP protocol access without the framework abstractions of Celery or Dramatiq.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; Direct control over AMQP exchanges, queues, bindings, and consumer acknowledgment. Useful for complex routing scenarios that require custom exchange configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational overhead:&lt;/strong&gt; Highest of the options listed. You're implementing consumer logic, retry handling, and DLQ routing in application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams that need fine-grained AMQP control and have the Python expertise to build consumer infrastructure from primitives.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose
&lt;/h2&gt;

&lt;p&gt;For most Python event-driven pipelines, the decision is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New pipeline, need reliability and monitoring:&lt;/strong&gt; Celery with Redis or RabbitMQ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler requirements, want minimal configuration:&lt;/strong&gt; RQ with Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celery complexity has been a problem, need middle ground:&lt;/strong&gt; Dramatiq&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building on RabbitMQ, want asyncio:&lt;/strong&gt; aio-pika&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Redis Streams control:&lt;/strong&gt; redis-py Streams API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few clarifying questions that narrow the choice faster:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Redis already in your stack?&lt;/strong&gt; If Redis is already running for caching, sessions, or rate limiting, using it as a message broker adds a queue use case to an existing service without introducing a new infrastructure dependency. Celery with Redis or RQ with Redis are both strong choices in this case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does your processing involve async I/O?&lt;/strong&gt; If the consumer code makes async database calls or API requests, aio-pika with asyncio handles more concurrent tasks per worker than a synchronous Celery pool. For CPU-bound work, synchronous Celery workers with a process pool are the standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much configuration complexity can the team absorb?&lt;/strong&gt; Celery's feature set is extensive and its configuration surface is large -- some settings interact in non-obvious ways. RQ is simpler but more limited. Dramatiq sits between them. If the team is less experienced with distributed task queues, starting with RQ and migrating to Celery when you hit a specific limitation is a practical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Commit: What to Validate
&lt;/h2&gt;

&lt;p&gt;Before finalizing your library choice, run these tests in a staging environment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kill a worker mid-task.&lt;/strong&gt; Send a message, wait for the worker to start processing, kill the process, and confirm the message is redelivered (not dropped) when the worker restarts. Celery with the default &lt;code&gt;task_acks_late=False&lt;/code&gt; acknowledges on receipt; with &lt;code&gt;task_acks_late=True&lt;/code&gt;, it acknowledges after execution. Know which behavior you have before it matters in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send a message that will fail every retry.&lt;/strong&gt; Confirm it ends up in a dead-letter destination (a DLQ list, a failed queue, or a visible error state) rather than disappearing silently. The DLQ behavior for exhausted-retry messages differs significantly between libraries and broker configurations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send a burst of 1,000 messages.&lt;/strong&gt; Watch queue depth and consumer lag. A consumer that processes 10 messages in a test may behave differently at 1,000 -- especially if you hit connection pool limits, rate limits on a downstream service, or memory limits in the broker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check what monitoring the library exposes.&lt;/strong&gt; Flower is the standard for Celery. rq-dashboard works with RQ. aio-pika and pika require custom instrumentation or broker-level metrics via the RabbitMQ management API. Confirm you can see queue depth and failure rates in your existing monitoring stack before committing to a library that makes this hard.&lt;/p&gt;

&lt;p&gt;These tests take a few hours. The alternative is discovering library-specific behaviors in production under load.&lt;/p&gt;

&lt;p&gt;The context for how these libraries fit into a full event-driven pipeline architecture -- from producer setup through consumer acknowledgment to failure handling -- is covered in &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt;'s guide "&lt;a href="https://137foundry.com/articles/how-to-build-event-driven-data-pipeline-python-message-queues" rel="noopener noreferrer"&gt;How to Build an Event-Driven Data Pipeline With Python and Message Queues&lt;/a&gt;". The code patterns in that guide use redis-py and Celery but the architectural decisions apply to any of the libraries listed here.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>data</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Set Up a Dead-Letter Queue in Celery With Redis</title>
      <dc:creator>137Foundry</dc:creator>
      <pubDate>Thu, 28 May 2026 10:27:57 +0000</pubDate>
      <link>https://dev.to/137foundry/how-to-set-up-a-dead-letter-queue-in-celery-with-redis-3n9n</link>
      <guid>https://dev.to/137foundry/how-to-set-up-a-dead-letter-queue-in-celery-with-redis-3n9n</guid>
      <description>&lt;p&gt;When a Celery task fails after all retries, where does it go? By default, Celery with a Redis broker just marks the task as failed and moves on. The failed task is logged, but it's not stored anywhere accessible -- and unless you're watching logs or have monitoring set up, you might not know it happened until something downstream breaks.&lt;/p&gt;

&lt;p&gt;Without a dead-letter queue, you have two choices when a task fails permanently: lose the data silently, or stop processing everything until the issue is manually fixed. Neither is acceptable for a production pipeline where missing even a small percentage of events can cause data integrity problems downstream.&lt;/p&gt;

&lt;p&gt;A dead-letter queue (DLQ) changes this: failed tasks get routed to a dedicated queue where you can inspect them, requeue them manually when the underlying issue is fixed, or log them for audit. Setting one up in Celery with &lt;a href="https://redis.io" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; requires a small amount of configuration but dramatically improves your ability to operate data pipelines reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Celery's Retry and Failure Flow
&lt;/h2&gt;

&lt;p&gt;Before configuring a DLQ, it helps to understand Celery's default behavior:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A task is called. It executes and either succeeds (returns a value) or fails (raises an exception).&lt;/li&gt;
&lt;li&gt;If it fails and &lt;code&gt;max_retries&lt;/code&gt; is set, Celery schedules a retry after the configured &lt;code&gt;countdown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it fails after &lt;code&gt;max_retries&lt;/code&gt; retries, Celery raises &lt;code&gt;MaxRetriesExceededError&lt;/code&gt;. The task enters &lt;code&gt;FAILURE&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;The failure is logged, the result is stored in the result backend (if configured), and nothing else happens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no built-in routing of failed tasks to a separate queue. You have to build that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Route to a DLQ Using task_failure Signal
&lt;/h2&gt;

&lt;p&gt;The cleanest approach for Redis-backed Celery is to use the &lt;code&gt;task_failure&lt;/code&gt; signal to route failed tasks to a dedicated Redis list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery.signals&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task_failure&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DLQ_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celery_dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@task_failure.connect&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_task_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;einfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dlq_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kwargs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DLQ_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dlq_entry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect this signal in your Celery app initialization. Every task that exhausts its retries will push an entry to the &lt;code&gt;celery_dlq&lt;/code&gt; list in Redis.&lt;/p&gt;

&lt;p&gt;You can then inspect DLQ contents from a management script or a monitoring dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inspect_dlq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DLQ_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Option 2: Dedicated DLQ Task Queue
&lt;/h2&gt;

&lt;p&gt;An alternative is to route failed tasks to a dedicated Celery queue rather than a raw Redis list. This allows DLQ items to be requeued as Celery tasks when the underlying issue is resolved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Celery&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery.signals&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task_failure&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Celery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379/0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dlq.failed_task&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dead_letter_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_task_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Store in Redis or log -- this task is for inspection, not re-processing
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DLQ: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;original_task_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] failed with: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@task_failure.connect&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_task_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dead_letter_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})],&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dlq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running a dedicated &lt;code&gt;celery worker -Q dlq&lt;/code&gt; processes only DLQ tasks. This separation keeps DLQ handling isolated from normal task workers and makes it visible as a queue in any Celery monitoring tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Custom Task Base Class With Built-In DLQ
&lt;/h2&gt;

&lt;p&gt;For a clean approach that doesn't require signal handlers, create a custom base task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DLQTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;abstract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;einfo&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traceback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;einfo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celery_dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;on_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;einfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DLQTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Processing logic
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any task that inherits from &lt;code&gt;DLQTask&lt;/code&gt; automatically routes failures to the DLQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Alerts on DLQ Depth
&lt;/h2&gt;

&lt;p&gt;A DLQ without monitoring is only marginally better than no DLQ. Add a simple depth check that runs on a schedule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_dlq_depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celery_dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Send alert -- Slack webhook, PagerDuty, email, etc.
&lt;/span&gt;        &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DLQ depth is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (threshold: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this every 5 minutes with a lightweight scheduler or as part of your existing monitoring. &lt;a href="https://www.datadoghq.com" rel="noopener noreferrer"&gt;Datadog&lt;/a&gt;, &lt;a href="https://prometheus.io" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt;, and most APM platforms can scrape &lt;a href="https://redis.io" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; metrics directly and alert on list length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-queuing DLQ Messages
&lt;/h2&gt;

&lt;p&gt;Once the underlying issue (bad data, downstream service outage, schema mismatch) is resolved, re-queuing DLQ messages means inspecting the failure entries and re-dispatching them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;requeue_dlq_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celery_dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;task_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;original_kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kwargs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="c1"&gt;# Re-dispatch -- requires task be importable by name
&lt;/span&gt;        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;original_kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Clear re-queued entries
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ltrim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celery_dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script reads up to 100 DLQ entries, re-dispatches them as Celery tasks, and removes them from the DLQ list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Include in Each DLQ Entry
&lt;/h2&gt;

&lt;p&gt;The DLQ entry is your primary debugging artifact. What you store determines how quickly you can diagnose and fix the underlying issue when you finally look at it.&lt;/p&gt;

&lt;p&gt;At minimum, every DLQ entry should include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The task name.&lt;/strong&gt; Which Celery task failed. This lets you group failures by task type: if all failures are from one task name, the issue is likely in that task's code or configuration. If failures span many task types, the issue is likely in the data or a shared dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The task ID.&lt;/strong&gt; The Celery task ID lets you cross-reference DLQ entries with your application logs. If you have structured logging in your tasks that includes task_id at start and completion, you can trace a failed task from the DLQ entry back to its full execution log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exception string.&lt;/strong&gt; &lt;code&gt;str(exc)&lt;/code&gt; is usually sufficient. The exception type tells you whether the failure is transient (network error, connection timeout) or permanent (schema validation error, missing required field).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The traceback.&lt;/strong&gt; For permanent failures, the traceback is the fastest path to the specific line of code that failed. The &lt;code&gt;einfo&lt;/code&gt; parameter in &lt;code&gt;on_failure&lt;/code&gt; provides a formatted traceback via &lt;code&gt;str(einfo)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The task arguments.&lt;/strong&gt; &lt;code&gt;str(args)&lt;/code&gt; and &lt;code&gt;str(kwargs)&lt;/code&gt; give you the inputs to the failed task. This is essential for debugging data-specific failures where only certain inputs trigger the error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure timestamp.&lt;/strong&gt; &lt;code&gt;datetime.utcnow().isoformat()&lt;/code&gt; at failure time. When you're reviewing DLQ contents hours after the failure occurred, knowing when each entry failed helps determine whether failures are ongoing or historical.&lt;/p&gt;

&lt;p&gt;All three options above include the core fields. If you're building a custom DLQ handler, start with &lt;code&gt;{task_name, task_id, exception, failed_at}&lt;/code&gt; as the minimum viable entry, and add traceback and args when you've encountered a debugging session where you wished you had them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Setting up a DLQ for Celery with Redis requires three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A mechanism to capture failed tasks (signal handler, base class override, or custom queue)&lt;/li&gt;
&lt;li&gt;Storage for failed task details (Redis list or dedicated Celery queue)&lt;/li&gt;
&lt;li&gt;Monitoring for DLQ depth with alerting&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The signal handler approach (Option 1) is the simplest to add to an existing pipeline. The custom base class approach (Option 3) is cleanest for new pipelines where every task should have DLQ behavior by default.&lt;/p&gt;

&lt;p&gt;A fourth piece worth adding: a requeue mechanism paired with a clear DLQ review process. The requeue script above reads DLQ entries and re-dispatches them as tasks after the underlying issue is fixed. Without that step, a DLQ is a collection bin for lost work rather than a recoverable buffer. Test the full loop -- failed tasks land in DLQ, alert fires, root cause is fixed, messages are requeued -- in staging before relying on it in production.&lt;/p&gt;

&lt;p&gt;The architectural context for why DLQs are necessary in event-driven pipelines -- and how they fit into the broader producer-consumer pattern -- is covered in the full guide from &lt;a href="https://137foundry.com" rel="noopener noreferrer"&gt;137Foundry&lt;/a&gt;: "&lt;a href="https://137foundry.com/articles/how-to-build-event-driven-data-pipeline-python-message-queues" rel="noopener noreferrer"&gt;How to Build an Event-Driven Data Pipeline With Python and Message Queues&lt;/a&gt;". That piece covers producer setup, consumer acknowledgment, and the failure handling design decisions that this implementation guide builds on.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>data</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
