<?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: qodors</title>
    <description>The latest articles on DEV Community by qodors (@qodors).</description>
    <link>https://dev.to/qodors</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3892554%2F90747c51-0df7-4e81-8950-b2d32b359d38.png</url>
      <title>DEV Community: qodors</title>
      <link>https://dev.to/qodors</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qodors"/>
    <language>en</language>
    <item>
      <title>AsNoTracking: The EF Core Setting Your Read Queries Are Missing</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Fri, 10 Jul 2026 08:38:53 +0000</pubDate>
      <link>https://dev.to/qodors/asnotracking-the-ef-core-setting-your-read-queries-are-missing-2dec</link>
      <guid>https://dev.to/qodors/asnotracking-the-ef-core-setting-your-read-queries-are-missing-2dec</guid>
      <description>&lt;p&gt;Most of the queries in a typical app are reads. You fetch data, show it, and never change it. EF Core doesn't know that. By default it assumes every entity you load might be modified and saved back, so it keeps track of all of them. That tracking has a cost, and on read-only queries you're paying it for nothing.&lt;/p&gt;

&lt;p&gt;AsNoTracking() turns it off for a query. The data comes back the same, minus the bookkeeping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Tracking Actually Does&lt;/strong&gt;&lt;br&gt;
When EF Core runs a normal query, it doesn't just hand you objects. It takes a snapshot of every entity and holds a reference to it in the change tracker. That's how SaveChanges() knows what changed — it compares the current state of each tracked entity against the snapshot it took when the entity was loaded.&lt;/p&gt;

&lt;p&gt;That's what you want when you're updating. When you load a customer, change their email, and call SaveChanges(), the tracker is what figures out that one field changed and writes the UPDATE.&lt;/p&gt;

&lt;p&gt;When you're loading a list of orders to render on a page, none of it matters. You're not saving anything, but EF Core still builds the snapshots and holds the references anyway, doing comparison work it will never use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the Cost Shows Up&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&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="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastWeek&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tracks every order it returns. Ten rows, fine. A few thousand rows on a reporting screen, and the tracking overhead becomes real — extra memory for the snapshots, extra CPU building them, and a change tracker now holding references to thousands of entities it'll never save.&lt;/p&gt;

&lt;p&gt;Add AsNoTracking() and that goes away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&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="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastWeek&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same SQL. Same data back. EF Core skips the snapshot and doesn't retain the entities. On large read queries the difference in memory and time is measurable, and it costs you one method call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Bug That Tracking Hides&lt;/strong&gt;&lt;br&gt;
There's a second reason this matters, and it's not about performance.&lt;/p&gt;

&lt;p&gt;Because the change tracker holds references to everything it loads, it also returns the same instance if you query the same row twice in one context. Sometimes that's convenient. Sometimes it hides a problem — you edit a tracked entity somewhere, and a completely separate read query later hands back your modified version instead of what's actually in the database, because the tracker gave you the cached instance.&lt;/p&gt;

&lt;p&gt;With AsNoTracking(), every query goes to the database and gives you a fresh object. For read paths, that's usually what you actually want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When You Should Not Use It&lt;/strong&gt;&lt;br&gt;
AsNoTracking() is for reads. The moment you plan to change an entity and save it, you need tracking on, because that's the mechanism that detects the change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstAsync&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;customer&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;newEmail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&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;SaveChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// works because customer is tracked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put AsNoTracking() on that query and SaveChanges() does nothing — EF Core isn't tracking the entity, so it has no idea anything changed. No error, no update, the email silently stays the same. The code looks correct, which is what makes it hard to find.&lt;/p&gt;

&lt;p&gt;So the line is simple: reading and displaying, use AsNoTracking(). Loading something to modify and save, don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doing It Everywhere Without Repeating Yourself&lt;/strong&gt;&lt;br&gt;
If most of your app is reads, adding AsNoTracking() to every query gets tedious and easy to forget. You can flip the default for a whole context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseQueryTrackingBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QueryTrackingBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoTracking&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now queries are no-tracking by default, and you opt back in on the queries that actually write, using .AsTracking(). For a read-heavy app this is often the better default — you stop paying for tracking everywhere and only turn it on where you need it.&lt;/p&gt;

&lt;p&gt;Just make sure the team knows the default changed. Someone who assumes tracking is on will write the update code above and watch SaveChanges() quietly do nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Take&lt;/strong&gt;&lt;br&gt;
At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=asnotracking_efcore" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, when we profile a read-heavy .NET app that's using more memory than it should, tracking is a regular offender. It rarely shows up as a crash or an obvious error. It shows up as a service that holds more memory than the data justifies, or a request that's slower than the size of its result set explains.&lt;/p&gt;

&lt;p&gt;It connects to something we've written about before — a query returning far more rows than it should because of the wrong IQueryable/IEnumerable return type. Fix that so you're loading the right rows, then add AsNoTracking() so you're not paying to track the read-only ones. The two together cover a large share of the EF Core performance problems we see.&lt;/p&gt;

&lt;p&gt;It's default behavior that happens to be correct for writes and wasteful for reads, running in an app that mostly reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Checklist&lt;/strong&gt;&lt;br&gt;
Add AsNoTracking() to queries that only read and display data&lt;br&gt;
Leave tracking on for any query where you'll modify the entity and call SaveChanges()&lt;br&gt;
For read-heavy apps, consider QueryTrackingBehavior.NoTracking as the context default and opt back in with .AsTracking()&lt;br&gt;
If SaveChanges() silently does nothing, check whether the entity was loaded with AsNoTracking()&lt;br&gt;
Watch for stale data from the change tracker returning a cached instance on read paths&lt;br&gt;
Pair it with correct IQueryable return types so you're tracking fewer, and the right, rows&lt;/p&gt;

&lt;p&gt;Tracking is on by default because EF Core can't tell a read from a write. On the queries where you're only reading, tell it. That's the whole optimization.&lt;/p&gt;

&lt;h1&gt;
  
  
  DotNet #CSharp #EFCore #EntityFramework #Performance #BackendDevelopment #SoftwareEngineering #DotNetCore #SQL #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we profile and fix slow, memory-hungry .NET apps. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>performance</category>
    </item>
    <item>
      <title>IQueryable vs IEnumerable: The Mistake That Loads Your Whole Table</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:40:48 +0000</pubDate>
      <link>https://dev.to/qodors/iqueryable-vs-ienumerable-the-mistake-that-loads-your-whole-table-nde</link>
      <guid>https://dev.to/qodors/iqueryable-vs-ienumerable-the-mistake-that-loads-your-whole-table-nde</guid>
      <description>&lt;p&gt;You wrote a clean LINQ query. It filters down to ten rows. It runs fine in dev, and then in production it's slow and your database CPU is higher than it should be for such a small result.&lt;br&gt;
The query looks right. The problem is where the filtering happens. With IEnumerable, the filter runs in your app after the whole table is already loaded from the database. With IQueryable, the filter becomes part of the SQL and runs in the database. Same LINQ, completely different amount of data moving across the wire.&lt;br&gt;
Get this wrong on a large table and you're pulling every row into memory to throw almost all of them away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Two Interfaces&lt;/strong&gt;&lt;br&gt;
Both let you write .Where(), .Select(), .OrderBy(). That's why they get mixed up. The difference is when and where the query executes.&lt;br&gt;
IEnumerable works on objects already in memory. When you call .Where() on it, the filtering happens in your application, in C#, row by row. If the data came from a database, it's already been loaded in full before your filter ever runs.&lt;br&gt;
IQueryable builds an expression tree instead of running anything immediately. EF Core takes that tree and translates it into SQL. Your .Where() becomes a SQL WHERE clause, and the database does the filtering before it sends anything back.&lt;br&gt;
Same method names. One filters in the database, the other filters in your app after the table is already loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where It Goes Wrong&lt;/strong&gt;&lt;br&gt;
Here's the line that causes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrders&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="n"&gt;_db&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="c1"&gt;// returns IEnumerable&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method returns IEnumerable. The moment you expose it as IEnumerable, any .Where() a caller adds runs in memory, not in SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetOrders&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastWeek&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That .Where() looks like it filters in the database. It doesn't. GetOrders() already returned an IEnumerable, so EF Core loads the entire Orders table into memory, and then C# filters it down to last week's rows. On a table with a few hundred rows you'd never notice. On a table with two million, it's a disaster.&lt;br&gt;
Now the version that does what you expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrders&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="n"&gt;_db&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="c1"&gt;// returns IQueryable&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 csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetOrders&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastWeek&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because GetOrders() returns IQueryable, the .Where() gets folded into the query, and EF Core generates SQL like this:&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;SELECT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;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;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&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="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;lastWeek&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database filters first and sends back only the rows you asked for. Same calling code. One returns two million rows over the wire, the other returns a handful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trap Is the Return Type&lt;/strong&gt;&lt;br&gt;
The mistake almost always hides in a method signature. Someone writes a repository or service method that returns IEnumerable because it feels safer or more general. Every caller that adds a filter after that point is filtering in memory, and nobody notices until the table grows.&lt;br&gt;
AsEnumerable() and ToList() do the same thing on purpose. The moment you call either, everything after it runs in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_db&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="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                              &lt;span class="c1"&gt;// whole table loaded here&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastWeek&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// filters in memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move the .Where() before the .ToList() and it runs in SQL. The order matters, and it's easy to get backwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When You Actually Want IEnumerable&lt;/strong&gt;&lt;br&gt;
This isn't "always use IQueryable." Once the data is in memory, IEnumerable is the right type, and forcing IQueryable where it doesn't belong just adds confusion.&lt;br&gt;
If you've already loaded a list and you're filtering it in code, that's IEnumerable and that's correct. If you're working with an in-memory collection that never touched a database, there's no SQL to translate to, so IQueryable buys you nothing. And there are queries EF Core can't translate to SQL — certain method calls or custom C# logic — where you have to pull the data into memory first and finish the work there. That's a real case, just do it deliberately, not by accident through a return type.&lt;br&gt;
The point isn't to fear IEnumerable. It's to know which one you're holding and where the filtering will run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Take&lt;/strong&gt;&lt;br&gt;
At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=iqueryable_vs_ienumerable" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, when a .NET app has queries that are slow out of proportion to the data they return, this is one of the first things we look at. The query reads fine. The LINQ is correct. The problem is a method three layers down that returns IEnumerable, quietly loading a whole table so the app can filter it in memory.&lt;br&gt;
It hides well because it doesn't fail. Small tables in development behave normally, tests pass, and it only turns into a problem when real data volume shows up. By then it looks like a database performance issue, and people go tuning indexes when the fix is a return type change from IEnumerable to IQueryable.&lt;br&gt;
Check what your repository and service methods return. If they hand back IEnumerable from an EF Core query, your filtering probably isn't happening where you think it is.&lt;br&gt;
Quick Checklist&lt;br&gt;
Return IQueryable from repository/service methods that wrap EF Core queries, so callers filter in SQL&lt;br&gt;
Watch every ToList() and AsEnumerable() — everything after it runs in memory&lt;br&gt;
Keep .Where() and .Select() before you materialize with ToList()&lt;br&gt;
Use IEnumerable for data that's already in memory — that's what it's for&lt;br&gt;
If a query genuinely can't translate to SQL, pull it into memory on purpose, not by accident&lt;br&gt;
When a query is slow for the rows it returns, check the return types up the call chain&lt;br&gt;
IQueryable and IEnumerable share the same LINQ methods, which is exactly why the bug is easy to write and hard to spot. One filters in the database. The other loads the table first and filters in your app.&lt;br&gt;
Know which one your method returns. That one decision is the difference between a query that touches ten rows and one that drags two million across the wire.&lt;/p&gt;

&lt;h1&gt;
  
  
  DotNet #CSharp #EFCore #LINQ #EntityFramework #BackendDevelopment #SoftwareEngineering #DotNetCore #Performance #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we fix .NET apps with queries slower than they should be. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>linq</category>
    </item>
    <item>
      <title>Dependency Injection in .NET Is Easy to Get Wrong</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Wed, 01 Jul 2026 09:53:11 +0000</pubDate>
      <link>https://dev.to/qodors/dependency-injection-in-net-is-easy-to-get-wrong-3176</link>
      <guid>https://dev.to/qodors/dependency-injection-in-net-is-easy-to-get-wrong-3176</guid>
      <description>&lt;p&gt;Almost every weird intermittent bug I've dealt with in a .NET app traced back to one place: a service registered with the wrong lifetime in Program.cs. Not the logic inside the service. The single line that said how long it should live.&lt;/p&gt;

&lt;p&gt;DI itself is built into .NET and easy to start with. You register your services, ask for them in a constructor, and the framework hands them over. The part that trips people up isn't wiring it together. It's the lifetimes.&lt;/p&gt;

&lt;p&gt;Get a lifetime wrong and the app still starts, requests still succeed, and everything looks healthy. The damage shows up later as stale data, or state bleeding between users, or memory that climbs and never drops back down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Three Lifetimes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are three, and the whole thing hinges on understanding them.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Transient *&lt;/em&gt;— you get a new instance every time you ask for one. Two things in the same request that both need it get two separate copies.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Scoped *&lt;/em&gt;— you get one instance per request. Everything in a single HTTP request shares the same instance, and the next request gets a fresh one.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Singleton *&lt;/em&gt;— you get one instance for the entire lifetime of the app. Every request, every user, shares the exact same object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEmailSender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICacheProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole API. Three methods. Picking the wrong one of the three is where most of the trouble comes from.&lt;br&gt;
&lt;strong&gt;The Captive Dependency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one that gets almost everybody at some point.&lt;/p&gt;

&lt;p&gt;You have a singleton. Inside it, you inject something scoped. It compiles, it runs, and it looks fine. But you've just trapped a scoped service inside a singleton.&lt;/p&gt;

&lt;p&gt;The singleton is created once and lives forever. The scoped service it grabbed gets held onto for that whole time, instead of being created fresh per request like it's supposed to. So now a service that was meant to live for one request is stuck living for the entire app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CacheProvider&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICacheProvider&lt;/span&gt;   &lt;span class="c1"&gt;// registered as Singleton&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// this is Scoped&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CacheProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// now the DbContext is captive&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&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;DbContext is the classic victim. It's registered scoped for a reason. It's not built to be shared across requests, it tracks changes and holds a connection, and when you trap it inside a singleton you get stale data, threading errors, and connection problems that are miserable to track down.&lt;/p&gt;

&lt;p&gt;The rule that keeps you safe: &lt;strong&gt;a service can only depend on things that live as long as it does, or longer&lt;/strong&gt;. A singleton can depend on a singleton. A scoped service can depend on scoped or singleton. But a singleton depending on scoped is the trap.&lt;/p&gt;

&lt;p&gt;The good news is that .NET can catch this for you. There's a scope validation check that throws at startup when a singleton depends on something scoped, and it's on by default in the Development environment. Don't turn it off. If it throws, it's telling you about a real bug before it reaches production.&lt;br&gt;
&lt;strong&gt;Singletons Holding State&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other common mistake is making something a singleton without thinking about what it holds.&lt;/p&gt;

&lt;p&gt;A singleton is shared across every request at the same time. If it has mutable state — a field it writes to, a dictionary it updates, a list it appends to — then multiple requests are hitting that state at once, on different threads. That's a race condition, and it shows up as data from one user leaking into another user's request, or numbers that don't add up.&lt;/p&gt;

&lt;p&gt;If a service is a singleton, either it holds no state, or the state it holds is safe for many threads to touch at once. A stateless helper is fine as a singleton. A cache built on a thread-safe collection is fine. A service that quietly keeps per-user data in a plain field is not.&lt;br&gt;
&lt;strong&gt;So Which One Do You Pick&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most things, the answer is scoped, and that's also the default for a reason.&lt;/p&gt;

&lt;p&gt;Scoped fits how web requests work. One instance per request, fresh state each time, cleaned up when the request ends. Your services that touch the database, handle business logic, or deal with the current user should almost always be scoped.&lt;/p&gt;

&lt;p&gt;Use transient for small, cheap, stateless things where a new instance every time costs nothing. Use singleton for things that are genuinely shared and either hold no state or are built to be thread-safe — configuration, a connection factory, an in-memory cache.&lt;/p&gt;

&lt;p&gt;When you're not sure, scoped is the safe default. Reaching for singleton to "save memory" is where people get into trouble, and the memory you save is almost never worth the bugs you buy.&lt;br&gt;
Our Take&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;&lt;a href="https://www.qodors.com/?utm_source=hashnode&amp;amp;utm_medium=post&amp;amp;utm_campaign=dotnet_di_lifetimes" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;&lt;/strong&gt;, when we're brought into a .NET app with weird intermittent bugs — stale data, state bleeding between users, memory that only grows — DI lifetimes are near the top of the list we check. It's rarely the code inside the service that's broken. It's how the service was registered.&lt;/p&gt;

&lt;p&gt;What makes it hard is that the wrong lifetime doesn't fail loudly. The app starts, requests succeed, everything looks healthy. The problem only surfaces under real concurrent traffic, and by then it looks like a dozen unrelated bugs instead of one registration mistake.&lt;/p&gt;

&lt;p&gt;Turn on scope validation, default to scoped, and think for a second before you make anything a singleton. Most DI problems never happen if you do those three things.&lt;br&gt;
&lt;strong&gt;Quick Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Default to scoped unless you have a clear reason not to&lt;/li&gt;
&lt;li&gt;    Never inject a scoped service into a singleton (the captive dependency)&lt;/li&gt;
&lt;li&gt;    A service can only depend on things that live as long as it does, or longer&lt;/li&gt;
&lt;li&gt;    Keep scope validation on in Development — don't silence it&lt;/li&gt;
&lt;li&gt;    Singletons must be stateless or genuinely thread-safe&lt;/li&gt;
&lt;li&gt;    DbContext stays scoped — don't trap it in a singleton&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The wrong lifetime is a one-line mistake that hides for weeks. It doesn't break the build and it doesn't throw in testing. It waits for real traffic, then hands you a pile of bugs that look unrelated until you trace them back to a single registration.&lt;/p&gt;

&lt;p&gt;Default to scoped, keep scope validation on, and be deliberate about singletons.&lt;/p&gt;

&lt;h1&gt;
  
  
  DotNet #CSharp #DependencyInjection #AspNetCore #BackendDevelopment #SoftwareEngineering #DotNetCore #StartupCTO #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we fix .NET apps with weird intermittent bugs. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>dotnetcore</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Using .Result and .Wait() in Your .NET Code</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:00:41 +0000</pubDate>
      <link>https://dev.to/qodors/stop-using-result-and-wait-in-your-net-code-4pln</link>
      <guid>https://dev.to/qodors/stop-using-result-and-wait-in-your-net-code-4pln</guid>
      <description>&lt;p&gt;Your app works fine in testing. You deploy it, a few users come on, and then it just... hangs. No error, no crash. The request sits there until it times out.&lt;/p&gt;

&lt;p&gt;You check the logs. Nothing useful. You restart the app and it's fine again for a while, then it locks up again under load.&lt;/p&gt;

&lt;p&gt;If you've got .Result or .Wait() anywhere in your code, that's very likely your problem. They look harmless. They're not.&lt;br&gt;
&lt;strong&gt;What These Actually Do&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;.Result and .Wait() take an async method and force it to run synchronously. You've got a Task and you want the value now, without making your method async, so you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetDataAsync&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles. It works on your machine. It looks like a quick way to avoid turning half your code async.&lt;/p&gt;

&lt;p&gt;What it actually does is block the current thread and tell it to sit and wait until the async operation finishes. And in certain setups, that wait never ends.&lt;br&gt;
&lt;strong&gt;The Deadlock&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's how it locks up.&lt;/p&gt;

&lt;p&gt;In older ASP.NET (and some desktop apps), there's a synchronization context that controls which thread continues after an await. When the async work finishes, it tries to come back to the original thread to continue.&lt;/p&gt;

&lt;p&gt;But that original thread is the one you blocked with .Result. It's sitting there waiting for the async work to complete. The async work is waiting for that thread to be free so it can finish. Neither one gives way.&lt;/p&gt;

&lt;p&gt;The request hangs. No exception, no log line, nothing. Just a thread stuck forever, and eventually a timeout. Do this enough times under load and you run out of threads, and the whole app stops responding.&lt;/p&gt;

&lt;p&gt;The worst part is it often doesn't show up in testing. One request at a time on your machine works fine. It's only under real traffic, with the thread pool under pressure, that it falls apart. So it passes your tests and breaks in production.&lt;br&gt;
&lt;strong&gt;The Fix Is Boring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't block. Go async the whole way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&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;await&lt;/span&gt; &lt;span class="nf"&gt;GetDataAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Make your method async, return a Task, and await the call instead of reaching for .Result.&lt;/p&gt;

&lt;p&gt;The rule people say for this is "async all the way." If a method calls something async, it should be async too, and so should the method that calls it, up the chain. Once one part of your code is async, that tends to spread upward, and that's fine. That's how it's supposed to work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCustomerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;customer&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;No blocking, no deadlock, and the thread is free to handle other requests while it waits.&lt;br&gt;
&lt;strong&gt;"But I Can't Make This Method Async"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the usual reason people reach for .Result. A constructor can't be async. A property getter can't be async. Some old interface you have to implement isn't async.&lt;/p&gt;

&lt;p&gt;Most of the time the real fix is to move the async work somewhere it belongs. Don't do database calls in a constructor. Don't run async work inside a property. If you've got async work stuck inside something that can't be async, that's usually a sign the work is in the wrong place, not a sign you need to block.&lt;/p&gt;

&lt;p&gt;There are a few genuine exceptions. The Main method in older console apps sometimes had to block (modern .NET lets Main be async, so this is mostly gone). And there are rare spots in framework-level code where you're truly stuck. But those are rare. If you're reaching for .Result in your normal request-handling code, it's almost never one of those cases.&lt;br&gt;
&lt;strong&gt;A Note on ConfigureAwait&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll see advice to use ConfigureAwait(false) to avoid the deadlock:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var data = await GetDataAsync().ConfigureAwait(false);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This tells the await it doesn't need to come back to the original thread, which sidesteps the deadlock. It's worth using in library code. But treat it as a safety net, not a fix. The actual fix is to not block in the first place. If your code is properly async all the way through, you don't have the deadlock to work around.&lt;br&gt;
&lt;strong&gt;Our Take&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=result_wait_deadlock" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, when a .NET app hangs under load with no error in the logs, .Result and .Wait() are one of the first things we grep for. It's a common one, and it's nasty precisely because it hides during testing and only shows up when real users arrive.&lt;/p&gt;

&lt;p&gt;The fix is rarely complicated. It's usually just making a chain of methods async that should have been async from the start. It feels like more work than slapping .Result on the end of a call, and that's exactly why people skip it. Then they spend a week chasing a production hang that a single await would have prevented.&lt;/p&gt;

&lt;p&gt;Async all the way isn't a style preference. It's how the thing is meant to be used.&lt;br&gt;
&lt;strong&gt;Quick Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Search your codebase for .Result and .Wait() — find every one&lt;/li&gt;
&lt;li&gt;    Anywhere in request-handling code, replace blocking with await&lt;/li&gt;
&lt;li&gt;    Make the calling methods async Task up the chain&lt;/li&gt;
&lt;li&gt;    If async work is stuck in a constructor or property, move it out&lt;/li&gt;
&lt;li&gt;    Use ConfigureAwait(false) in library code as a safety net, not a substitute for going async&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;.Result and .Wait() are quick to type and they'll pass your tests. Then they hang in production with no error to point you at the cause.&lt;/p&gt;

&lt;p&gt;Go async properly and the problem never shows up. That's the whole trade.&lt;/p&gt;

&lt;h1&gt;
  
  
  DotNet #CSharp #AsyncAwait #AspNetCore #BackendDevelopment #SoftwareEngineering #Performance #DotNetCore #StartupCTO #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we fix .NET apps that hang under load. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>dotnetcore</category>
      <category>performance</category>
    </item>
    <item>
      <title>Entity Framework Is Slow. It's Not EF's Fault.</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Sat, 20 Jun 2026 12:00:56 +0000</pubDate>
      <link>https://dev.to/qodors/entity-framework-is-slow-its-not-efs-fault-5ana</link>
      <guid>https://dev.to/qodors/entity-framework-is-slow-its-not-efs-fault-5ana</guid>
      <description>&lt;p&gt;Your API was quick in development. Then traffic picks up and a few endpoints start taking three seconds to respond.&lt;/p&gt;

&lt;p&gt;You open the code, see Entity Framework everywhere, and figure that's the culprit. Time to rip it out and write proper SQL.&lt;/p&gt;

&lt;p&gt;Don't. Before you throw away the thing saving you thousands of lines of code, look at what it's actually doing. Most of the time EF isn't slow. It's doing exactly what your code asked, and your code asked for something expensive.&lt;br&gt;
&lt;strong&gt;EF Does What You Tell It&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Entity Framework writes SQL for you. That's the point, and it's useful. The downside is that one clean-looking line of C# can turn into a query that hammers your database, and nothing in the code tells you that's happening.&lt;/p&gt;

&lt;p&gt;The SQL is hidden, so the cost is hidden too. That's usually where things go wrong.&lt;/p&gt;

&lt;p&gt;Below are the four things that actually slow EF down. None of them are EF being slow.&lt;br&gt;
&lt;strong&gt;1. The N+1 Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the big one. Almost every slow EF app has it somewhere.&lt;/p&gt;

&lt;p&gt;You load a list of orders, loop through them, and read order.Customer for each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&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;context&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="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF didn't load the customers when it loaded the orders. So the first line runs one query, and then every time you touch order.Customer it goes back to the database for that one customer. A hundred orders, a hundred and one queries.&lt;/p&gt;

&lt;p&gt;Tell EF to load the customers with the orders and the problem goes away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&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;context&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="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same data on screen, one query instead of a hundred and one.&lt;br&gt;
&lt;strong&gt;2. Loading Whole Entities for Two Fields&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Say you need a list of customer names and emails for a page. The easy version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF pulls every column of every customer and builds a full object for each. Addresses, notes, timestamps, everything. You needed a name and an email.&lt;/p&gt;

&lt;p&gt;Project down to what you'll actually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&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;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&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="n"&gt;Name&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="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's two columns. Less data read, less memory spent building objects you were never going to look at.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3. Tracking You Don't Need
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF tracks every entity it loads so it can figure out what changed when you call SaveChanges. That's needed when you're updating something. It's pure overhead when you're just reading data to show it on a screen and you'll never touch it again.&lt;/p&gt;

&lt;p&gt;For read-only queries, switch tracking off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&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="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a large result set this matters, because EF stops building and holding all the change-tracking state it was never going to use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Filtering in C# Instead of the Database&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one is easy to miss and it hurts badly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;activeUsers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ToList() runs first. That pulls every user into memory, and only then does the Where filter them in C#. Two million users, fifty active, and you've loaded all two million to keep fifty.&lt;/p&gt;

&lt;p&gt;Move the filter ahead of ToList() so it ends up in the SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;activeUsers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database filters and hands you fifty rows. Same two lines, swapped order, completely different behaviour under load.&lt;br&gt;
See What EF Is Doing&lt;/p&gt;

&lt;p&gt;You don't have to guess about any of this. EF can log the SQL it generates. Turn it on in your DbContext setup:&lt;/p&gt;

&lt;p&gt;optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);&lt;/p&gt;

&lt;p&gt;Run the app, hit the slow endpoint, read the output. The same SELECT firing over and over in a loop is your N+1. A query dragging out thirty columns when you needed two is your projection problem. It's all there.&lt;/p&gt;

&lt;p&gt;Most teams never look. They treat EF as a black box and blame it when things get slow. It isn't a black box. The SQL is sitting in the logs the whole time.&lt;br&gt;
When Raw SQL Is Actually Worth It&lt;/p&gt;

&lt;p&gt;EF isn't always the right call. A heavy reporting query with several joins, grouping and aggregation can genuinely be faster and clearer as hand-written SQL or a stored procedure. Use raw SQL there. EF doesn't have to handle everything.&lt;/p&gt;

&lt;p&gt;But that's a small part of a normal app. The everyday reads and writes that make up most of your code are fine in EF once you've stopped tripping over the four things above. Rewriting your whole data layer because a handful of queries are slow is a huge amount of work to fix something that usually lives in four or five places.&lt;/p&gt;

&lt;p&gt;Find the slow queries first. Then decide.&lt;br&gt;
Our Take&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=ef_performance" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, when a client says EF is slow and wants it gone, the first thing we ask for is the generated SQL. Nobody writes raw SQL until we've seen that.&lt;/p&gt;

&lt;p&gt;It's almost always the same short list. An N+1 that needs an Include. A query loading full entities where a projection would do. Read-only queries that should be AsNoTracking. A filter sitting on the wrong side of a ToList(). Fixing those is usually a day of work, and it gets you most of the speed people were hoping a full rewrite would buy.&lt;/p&gt;

&lt;p&gt;EF is a tool. Hand it careless code and it produces expensive queries. That's worth understanding before you decide the framework is the problem.&lt;br&gt;
Before You Rip Out EF&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Turn on SQL logging and read what EF actually generates&lt;/li&gt;
&lt;li&gt;    Look for the same query firing in a loop, then fix it with Include&lt;/li&gt;
&lt;li&gt;    Check whether you're loading full entities where a Select would do&lt;/li&gt;
&lt;li&gt;    Make sure read-only queries use AsNoTracking&lt;/li&gt;
&lt;li&gt;    Check for any .Where sitting after a .ToList() instead of before it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EF will happily generate bad SQL if your code asks for it. The fix is almost never ripping it out. Read the queries first. Most teams find the framework was never the problem.&lt;/p&gt;

&lt;h1&gt;
  
  
  DotNet #EntityFramework #CSharp #EFCore #AspNetCore #BackendDevelopment #SoftwareEngineering #Performance #StartupCTO #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we make slow .NET systems fast without rewrites. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>performance</category>
    </item>
    <item>
      <title>Technical Debt: The Bill Always Comes Due</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Wed, 17 Jun 2026 11:28:40 +0000</pubDate>
      <link>https://dev.to/qodors/technical-debt-the-bill-always-comes-due-1ofd</link>
      <guid>https://dev.to/qodors/technical-debt-the-bill-always-comes-due-1ofd</guid>
      <description>&lt;p&gt;You needed to ship fast. So you took shortcuts.&lt;/p&gt;

&lt;p&gt;You hardcoded a few values. Skipped some tests. Copy-pasted a function instead of cleaning it up. Dropped a // TODO: fix this later and kept moving.&lt;/p&gt;

&lt;p&gt;And it worked. You shipped, the feature went live, the deadline got hit.&lt;/p&gt;

&lt;p&gt;That was six months ago. These days every new feature takes longer than it should. You change something small and something unrelated breaks. A new developer joined last month and still can't work in half the codebase because nobody can really explain how it fits together. The shortcuts you took back then are the reason.&lt;br&gt;
&lt;strong&gt;What Technical Debt Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technical debt is the cost you pay later for moving fast now.&lt;/p&gt;

&lt;p&gt;It isn't automatically bad. Sometimes a shortcut is the right call. A startup that ships a slightly messy product and survives beats a perfect one that ships late and runs out of runway. Nobody should feel guilty about that trade.&lt;/p&gt;

&lt;p&gt;The trouble starts when you take on debt without realizing it, and then never go back to deal with it.&lt;/p&gt;

&lt;p&gt;Some debt you choose. You knew the clean way to do it, you had a deadline, and you picked the fast way on purpose. That's fine, assuming you actually circle back.&lt;/p&gt;

&lt;p&gt;The other kind nobody chooses. The code just drifts. Patterns get copied without thought, old decisions stop making sense, and no one updates them. This is the kind that does the real damage, because there's no decision anywhere and nobody's keeping track of it. It just accumulates while you're busy with other things.&lt;br&gt;
How You Know It's Piling Up&lt;/p&gt;

&lt;p&gt;You can usually feel it before you can measure it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    A small change takes far longer than it should&lt;/li&gt;
&lt;li&gt;    Fixing one thing breaks two others&lt;/li&gt;
&lt;li&gt;    People are nervous about touching certain files&lt;/li&gt;
&lt;li&gt;    New hires take weeks to get productive&lt;/li&gt;
&lt;li&gt;    "Don't touch that, it works and nobody knows why"&lt;/li&gt;
&lt;li&gt;    More of your time goes to maintenance than to building anything new&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A team that gets slower every month instead of faster usually isn't a team problem. It's the codebase quietly fighting back.&lt;br&gt;
&lt;strong&gt;Why It Gets Expensive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Debt is cheap at the start, which is exactly what makes it dangerous.&lt;/p&gt;

&lt;p&gt;One messy function is nothing. You work around it. But messy code tends to breed more of it, because the next person copies whatever's already there. One shortcut turns into ten, and ten turns into a system nobody fully understands anymore.&lt;/p&gt;

&lt;p&gt;Then it starts costing you in ways you can actually measure. Features that used to take two days take a week, because the code fights you the whole way through. Bugs go up, since touching one thing breaks another, and you burn time fixing regressions instead of building. Good engineers get tired of it and leave, because nobody wants to spend their days wrestling a codebase nobody maintains. And when a competitor ships something in a week, you need a month, because your foundation can't carry the weight.&lt;/p&gt;

&lt;p&gt;That's the part that stings. The mess you accepted to move fast eventually becomes the thing stopping you from moving fast.&lt;br&gt;
&lt;strong&gt;How to Actually Deal With It&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't fix this by halting everything for a big rewrite. That almost never works, and it's a separate conversation. You handle it gradually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make it visible&lt;/strong&gt;. You can't manage what nobody can see. When someone takes a shortcut, write it down somewhere — a ticket, a list, a doc, whatever your team will actually look at. The most dangerous debt is the kind nobody is tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pay a little, regularly&lt;/strong&gt;. Don't wait for the cleanup sprint that never gets scheduled. Carve out a slice of every cycle, somewhere around 15 to 20 percent, for chipping away at it. Small steady effort gets further than one heroic fix that keeps getting pushed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean what you're already in&lt;/strong&gt;. When you're working in a messy file anyway, tidy it up a little before you leave. Not a rewrite, just a bit better than you found it. Over time the areas you touch most often quietly get cleaner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take on new debt deliberately&lt;/strong&gt;. Some shortcuts are worth it. Just make it a real decision instead of an accident. "We're cutting this corner to hit the launch and fixing it in Q2" is a plan. Messy code with no plan behind it is just a problem waiting to grow.&lt;br&gt;
Our Take&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=technical_debt" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, we don't tell clients to avoid technical debt. That advice isn't realistic and it isn't even good. Speed matters, especially early on.&lt;/p&gt;

&lt;p&gt;What we tell them is to know what they're taking on and deal with it before it starts dictating the roadmap. The teams that are still shipping fast in year three aren't the ones who wrote perfect code from day one. They're the ones who didn't let the shortcuts quietly stack up into a wall.&lt;/p&gt;

&lt;p&gt;Handled deliberately, debt is just part of building software. Ignored, it sets the pace for everything you do next.&lt;br&gt;
&lt;strong&gt;Questions Worth Asking Your Team&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Do you actually know where your biggest debt is, or is it just "everywhere"?&lt;/li&gt;
&lt;li&gt;    Are you paying any of it down regularly, or only when something breaks?&lt;/li&gt;
&lt;li&gt;    Has the team been getting slower month over month? Do you know why?&lt;/li&gt;
&lt;li&gt;    When someone takes a shortcut, does it get written down anywhere?&lt;/li&gt;
&lt;li&gt;    Is there a file everyone avoids? That's probably your most expensive debt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Borrowing speed is fine. Most teams should.&lt;/p&gt;

&lt;p&gt;Just don't forget you borrowed it, because the longer that sits, the more it ends up costing you.&lt;/p&gt;

&lt;h1&gt;
  
  
  TechnicalDebt #SoftwareEngineering #EngineeringManagement #StartupCTO #TechLeadership #CodeQuality #SoftwareDevelopment #EngineeringCulture #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we help startups keep their codebase fast as they grow. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>technicaldebt</category>
      <category>softwareengineering</category>
      <category>startup</category>
      <category>codequality</category>
    </item>
    <item>
      <title>Your Database Is Fast. Your Queries Are Slow.</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Wed, 10 Jun 2026 12:34:33 +0000</pubDate>
      <link>https://dev.to/qodors/your-database-is-fast-your-queries-are-slow-gg5</link>
      <guid>https://dev.to/qodors/your-database-is-fast-your-queries-are-slow-gg5</guid>
      <description>&lt;p&gt;Your app was fast when you launched. A few months later, it's slow.&lt;/p&gt;

&lt;p&gt;So you upgrade the database. More CPU, more RAM, a bigger plan. It gets better for a week, then it's slow again.&lt;/p&gt;

&lt;p&gt;Most teams upgrade a second time here. Don't.&lt;/p&gt;

&lt;p&gt;The database is probably fine. Postgres can handle millions of rows without a problem. The issue is how your app asks for the data.&lt;br&gt;
The Real Problems&lt;/p&gt;

&lt;p&gt;When an app gets slow, people blame the server. Usually it's one of these four things. None of them get fixed by a bigger server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Missing Indexes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the most common one.&lt;/p&gt;

&lt;p&gt;Run WHERE email = '&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;' on a table with no index, and Postgres has to check every row to find the match. With 500 rows, you won't notice. With 5 million, you wait.&lt;/p&gt;

&lt;p&gt;An index is like the index in a book. Instead of reading every page, you go straight to the right one.&lt;/p&gt;

&lt;p&gt;sql&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_email&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line can take a query from 4 seconds to a few milliseconds. No new server needed.&lt;/p&gt;

&lt;p&gt;Don't add an index to every column though. Each one slows down writes a little and uses storage. Add them to the columns you search, join, and sort on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The N+1 Query Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one hides inside your ORM.&lt;/p&gt;

&lt;p&gt;You load 100 posts. Then your code reads post.author for each one. That's 1 query for the posts and 100 more for the authors. 101 queries to load one page. And your code looks normal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;1 query:     SELECT * FROM posts LIMIT 100&lt;br&gt;
100 queries: SELECT * FROM users WHERE id = ?   (one per post)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The fix is to load the authors all at once:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&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="mi"&gt;2&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two queries instead of 101. Your ORM can do this. It's usually called eager loading. You just have to remember to use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. SELECT * Everywhere&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You need a name and an email. You write SELECT * because it's quick. Now you're pulling all 30 columns, including a big JSON field and a base64 image you don't need.&lt;/p&gt;

&lt;p&gt;You're sending megabytes to use two fields.&lt;/p&gt;

&lt;p&gt;sql&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&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="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask for what you need. It reads less from disk, sends less over the network, uses less memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No Pagination&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SELECT * FROM orders is fine with 50 rows on your laptop.&lt;/p&gt;

&lt;p&gt;In production with 2 million orders, it tries to load all of them at once. The query times out or the server runs out of memory. Nobody needs 2 million rows on one screen.&lt;/p&gt;

&lt;p&gt;sql&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show 20. Let them load the next 20.&lt;br&gt;
Don't Guess. Use EXPLAIN.&lt;/p&gt;

&lt;p&gt;You don't have to guess which query is slow. Postgres tells you.&lt;/p&gt;

&lt;p&gt;sql&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;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at one thing:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan — Postgres is reading the whole table. On a big table, that's your problem.
Index Scan — Postgres is using an index. Good.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;See a Seq Scan on a big table in a slow query? Add an index. You found it.&lt;br&gt;
Why a Bigger Server Doesn't Help&lt;/p&gt;

&lt;p&gt;A bigger server makes a slow query a little less slow. It doesn't fix it.&lt;/p&gt;

&lt;p&gt;A missing index means scanning millions of rows. A faster CPU scans those rows a bit faster, but it still scans all of them. You pay more to do the same wasted work. The N+1 page that runs 101 queries still runs 101 queries on a bigger box. More RAM doesn't make your code ask for data better.&lt;/p&gt;

&lt;p&gt;A bigger server buys you time. Sometimes that's worth it before a launch. But it's a patch, and it's expensive. You still have to fix the query later.&lt;br&gt;
Our Take&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=slow_queries" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, when a client says their database is slow, the server size is the last thing we check, not the first.&lt;/p&gt;

&lt;p&gt;Almost every time, the fix is in the queries:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Indexes on the columns that get used
Fixing N+1 with eager loading
Selecting only the columns you need
Paginating anything that can grow
Reading EXPLAIN ANALYZE instead of guessing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;None of this costs money. It costs some time. And it usually beats doubling your server.&lt;/p&gt;

&lt;p&gt;Your database can handle far more than you're giving it. The real question is whether your queries are getting in the way.&lt;br&gt;
Before You Upgrade&lt;/p&gt;

&lt;p&gt;Check these first:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are your common queries indexed? Run EXPLAIN and look for Seq Scan on big tables.
How many queries does one page run? Count them. The number is often higher than you think.
Are you pulling 30 columns to use 2?
Is anything unpaginated that could grow large?
Have you profiled it, or are you about to buy RAM on a guess?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A bigger server hides the problem.&lt;/p&gt;

&lt;p&gt;Fixing the queries removes it.&lt;/p&gt;

&lt;p&gt;Start with the queries. Most teams find they had enough database the whole time.&lt;/p&gt;

&lt;h1&gt;
  
  
  Database #PostgreSQL #SQL #PerformanceOptimization #BackendDevelopment #SoftwareEngineering #StartupCTO #TechLeadership #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we make slow systems fast without throwing money at hardware. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgressql</category>
      <category>sql</category>
      <category>performance</category>
    </item>
    <item>
      <title>Your Microservices Aren't Microservices</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Fri, 05 Jun 2026 09:40:35 +0000</pubDate>
      <link>https://dev.to/qodors/your-microservices-arent-microservices-598o</link>
      <guid>https://dev.to/qodors/your-microservices-arent-microservices-598o</guid>
      <description>&lt;p&gt;You split your monolith into 12 services. You're doing microservices now.&lt;/p&gt;

&lt;p&gt;Except every service calls every other service. Deploy one, you have to deploy all of them. One goes down, the whole system goes down.&lt;/p&gt;

&lt;p&gt;You didn't build microservices. You built a distributed monolith.&lt;/p&gt;

&lt;p&gt;You took a system that ran in one place and scattered it across the network — keeping all the tight coupling, but adding latency, failure points, and DevOps pain on top.&lt;/p&gt;

&lt;p&gt;This is the worst of both worlds. And most teams don't realize they're in it until production is on fire.&lt;br&gt;
&lt;strong&gt;How You Got Here&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It usually starts with good intentions.&lt;/p&gt;

&lt;p&gt;The monolith felt slow and scary. Microservices were the answer everyone preached. So you started splitting.&lt;/p&gt;

&lt;p&gt;But you split by &lt;strong&gt;technical layers&lt;/strong&gt;, not by &lt;strong&gt;business domains&lt;/strong&gt;. You created a "database service," an "auth service," a "notification service" — and they all depend on each other to do anything useful.&lt;/p&gt;

&lt;p&gt;Now placing an order touches eight services in a single request. Each call adds network latency. Each service is a new place to fail.&lt;/p&gt;

&lt;p&gt;You didn't decouple anything. You just added network calls between functions that used to be a simple method call.&lt;br&gt;
&lt;strong&gt;The Signs You Have a Distributed Monolith&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Be honest. How many of these are true?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    You can't deploy one service without deploying others&lt;/li&gt;
&lt;li&gt;    One service going down takes the whole system with it&lt;/li&gt;
&lt;li&gt;    A single user action triggers a chain of 5+ service calls&lt;/li&gt;
&lt;li&gt;    Services share the same database&lt;/li&gt;
&lt;li&gt;    You need all services running locally just to test one feature&lt;/li&gt;
&lt;li&gt;    Changing one service's API breaks three others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you nodded at three or more, you don't have microservices. You have a monolith with extra network latency and a much harder deployment story.&lt;br&gt;
&lt;strong&gt;Why This Is Worse Than a Monolith&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A regular monolith has one real advantage: simplicity.&lt;/p&gt;

&lt;p&gt;One deploy. One process. Function calls are instant. Debugging is straightforward. Transactions are easy.&lt;/p&gt;

&lt;p&gt;A distributed monolith throws all of that away and gives you nothing in return:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Network latency **on every internal call
**Partial failures **— service A succeeds, service B fails, now your data is inconsistent
**Distributed debugging **— tracing a bug across 8 services and 4 logs
**Deployment coordination **— you can't ship one thing without shipping everything
**Operational overhead **— monitoring, scaling, and securing 12 services instead of 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You took on all the complexity of distributed systems and kept all the coupling of a monolith.&lt;br&gt;
&lt;strong&gt;What Real Microservices Look Like&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;True microservices are independent. That's the entire point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Split by Business Domain, Not Technical Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't build a "database service." Build an "Orders service" that owns everything about orders — its logic, its data, its rules.&lt;/p&gt;

&lt;p&gt;Each service is a complete vertical slice of business capability. It can do its job without constantly asking others for help.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Each Service Owns Its Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No shared databases. The Orders service has its own database. The Inventory service has its own. They never reach into each other's tables.&lt;/p&gt;

&lt;p&gt;If Orders needs inventory data, it asks through an API or an event — it doesn't query Inventory's database directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Services Communicate Loosely&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where possible, use asynchronous events instead of synchronous calls.&lt;/p&gt;

&lt;p&gt;When an order is placed, the Orders service emits an OrderPlaced event. Inventory and Notifications react to it on their own. Orders doesn't wait. Orders doesn't even know who's listening.&lt;/p&gt;

&lt;p&gt;If Notifications is down, orders still go through. That's independence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Independent Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You should be able to deploy the Orders service at 2 PM on a Tuesday without touching anything else. No coordination. No "deploy everything together" ritual.&lt;/p&gt;

&lt;p&gt;If you can't, your services are still coupled.&lt;br&gt;
&lt;strong&gt;The Honest Truth About Microservices&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most startups don't need microservices at all.&lt;/p&gt;

&lt;p&gt;A well-structured monolith — clean modules, clear boundaries, one database — will take you further than you think. Often all the way to serious scale.&lt;/p&gt;

&lt;p&gt;Microservices solve organizational problems more than technical ones. They let large teams work independently on separate parts of a system. If you have 8 engineers, you probably don't have that problem yet.&lt;/p&gt;

&lt;p&gt;Splitting too early gives you all the complexity of distributed systems with none of the benefits. You spend your time managing infrastructure instead of building product.&lt;br&gt;
Our Take&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=distributed_monolith" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, we've seen more startups hurt by premature microservices than helped by them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The right path is usually:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Start with a clean, modular monolith&lt;/li&gt;
&lt;li&gt;    Draw clear boundaries between business domains inside it&lt;/li&gt;
&lt;li&gt;    Extract a service only when you have a real reason — a piece that needs independent scaling, a team that needs independence, a component with different reliability needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you do split, split by domain, give each service its own data, and make them communicate loosely.&lt;/p&gt;

&lt;p&gt;Microservices are a tool, not a trophy. Use them when the problem demands it — not because a blog post told you monoliths are dead.&lt;br&gt;
&lt;strong&gt;Before You Split Your Next Service&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Five questions to ask first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Are you splitting by business domain or technical layer? Domain is right. Layer is a trap.&lt;/li&gt;
&lt;li&gt;    Will this service own its own data? If it shares a database, it's not independent.&lt;/li&gt;
&lt;li&gt;    Can it be deployed alone? If not, you've just added a network call.&lt;/li&gt;
&lt;li&gt;    Can it survive other services being down? Real services degrade gracefully.&lt;/li&gt;
&lt;li&gt;    Do you actually need this split? Or would a clean module in your monolith do the job?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microservices aren't about having many services.&lt;/p&gt;

&lt;p&gt;They're about having **independent **ones.&lt;/p&gt;

&lt;p&gt;Get that wrong, and you've just made your monolith slower and harder to deploy.&lt;/p&gt;

&lt;h1&gt;
  
  
  Microservices #SoftwareArchitecture #DistributedSystems #SystemDesign #StartupCTO #TechLeadership #BackendDevelopment #SoftwareEngineering #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we help startups build architecture that actually scales. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Your API Design Is Why Integrations Take Forever</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Tue, 02 Jun 2026 09:44:30 +0000</pubDate>
      <link>https://dev.to/qodors/your-api-design-is-why-integrations-take-forever-1f38</link>
      <guid>https://dev.to/qodors/your-api-design-is-why-integrations-take-forever-1f38</guid>
      <description>&lt;p&gt;A developer wants to integrate with your API. You expect it to take two days.&lt;/p&gt;

&lt;p&gt;Two weeks later, they're still fighting it.&lt;/p&gt;

&lt;p&gt;Missing documentation. Inconsistent responses. No pagination. Cryptic error messages. Authentication that makes no sense.&lt;/p&gt;

&lt;p&gt;Your API works. But working isn't the same as usable.&lt;/p&gt;

&lt;p&gt;Bad API design doesn't just frustrate developers. It kills deals, delays launches, and quietly costs you customers who give up and find an alternative.&lt;br&gt;
Why APIs Become a Nightmare&lt;/p&gt;

&lt;p&gt;Most APIs aren't designed. They grow.&lt;/p&gt;

&lt;p&gt;You build one endpoint. Then another. Then a feature needs a quick addition, so you bolt it on. Six months later, you have 40 endpoints that all behave differently.&lt;/p&gt;

&lt;p&gt;One returns userId. Another returns user_id. A third returns id. Same data, three formats.&lt;/p&gt;

&lt;p&gt;Errors come back as plain text sometimes, JSON other times. Status codes are random. A failed request returns 200 with an error message buried in the body.&lt;/p&gt;

&lt;p&gt;Developers integrating with this don't read your mind. They read your responses. And your responses are chaos.&lt;br&gt;
The 5 Fixes That Save Weeks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Consistent Naming and Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick a convention. Stick to it everywhere.&lt;/p&gt;

&lt;p&gt;If you use snake_case, use it for every field, every endpoint, forever. If your list endpoints return { "data": [...] }, every list endpoint returns that exact shape.&lt;/p&gt;

&lt;p&gt;Consistency means developers learn your API once, then predict the rest. Inconsistency means they check the docs for every single call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Proper Error Responses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Errors are not an afterthought. They're half your API.&lt;/p&gt;

&lt;p&gt;Every error needs a consistent structure:&lt;/p&gt;

&lt;p&gt;json&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{&lt;br&gt;
  "error": {&lt;br&gt;
    "code": "INVALID_EMAIL",&lt;br&gt;
    "message": "The email format is invalid",&lt;br&gt;
    "field": "email"&lt;br&gt;
  }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use correct HTTP status codes. 400 for bad input. 401 for auth failures. 404 for missing resources. 429 for rate limits. Don't return 200 with an error inside.&lt;/p&gt;

&lt;p&gt;A developer should know what broke and how to fix it — without contacting your support team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Pagination From Day One&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That endpoint returning all records works fine with 50 rows. At 50,000 rows, it times out and crashes.&lt;/p&gt;

&lt;p&gt;Build pagination into every list endpoint from the start. Cursor-based or offset-based, pick one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /users?limit=20&amp;amp;cursor=eyJpZCI6MTAwfQ&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Retrofitting pagination later means breaking every existing integration. Do it now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Versioning Strategy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The day you change a response format, every integration breaks.&lt;/p&gt;

&lt;p&gt;Version your API from the beginning:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/v1/users&lt;br&gt;
/v2/users&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When you need breaking changes, ship v2 and keep v1 alive. Give developers time to migrate. Don't break production on a Tuesday afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Documentation That Actually Helps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A reference list of endpoints isn't documentation.&lt;/p&gt;

&lt;p&gt;Real documentation has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Working code examples in multiple languages&lt;/li&gt;
&lt;li&gt;    Real request and response samples&lt;/li&gt;
&lt;li&gt;    Authentication walkthroughs&lt;/li&gt;
&lt;li&gt;    Common error scenarios&lt;/li&gt;
&lt;li&gt;    A way to test calls live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a developer can integrate without messaging you, your docs work. If your inbox fills with "how do I..." questions, they don't.&lt;br&gt;
&lt;strong&gt;The Hidden Cost of Bad APIs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every confusing endpoint is a support ticket.&lt;/li&gt;
&lt;li&gt;Every missing example is a delayed integration.&lt;/li&gt;
&lt;li&gt;Every breaking change without versioning is an angry customer.&lt;/li&gt;
&lt;li&gt;Every inconsistent response is hours of debugging on someone else's side.
Bad API design doesn't show up on your dashboard. It shows up in churn, slow sales cycles, and partners who quietly walk away.
Our Take&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=api_design" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, we treat API design as product design. Because for developers, your API is your product.&lt;/p&gt;

&lt;p&gt;A well-designed API gets integrated in hours. A bad one gets abandoned in weeks.&lt;/p&gt;

&lt;p&gt;The difference isn't more features. It's consistency, clear errors, proper pagination, smart versioning, and documentation that respects the developer's time.&lt;/p&gt;

&lt;p&gt;Most teams skip this because it's not glamorous. That's exactly why doing it well sets you apart.&lt;br&gt;
If Your Integrations Take Too Long&lt;/p&gt;

&lt;p&gt;Five questions to ask your team:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;    Is every endpoint consistent? Same naming, same response shape, same patterns.&lt;/li&gt;
&lt;li&gt;    Are your errors structured and clear? Right status codes, helpful messages, actionable details.&lt;/li&gt;
&lt;li&gt;    Does every list endpoint paginate? Or will it crash at scale?&lt;/li&gt;
&lt;li&gt;    Do you have a versioning strategy? Or does every change break someone?&lt;/li&gt;
&lt;li&gt;    Can a developer integrate without contacting you? That's the real documentation test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your API isn't just code. It's an experience.&lt;/p&gt;

&lt;p&gt;Make it one developers actually want to use.&lt;/p&gt;

&lt;h1&gt;
  
  
  APIDesign #SoftwareEngineering #DeveloperExperience #BackendDevelopment #StartupCTO #TechLeadership #APIDevelopment #EngineeringBestPractices #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Bottom CTA:&lt;br&gt;
Written by the team at Qodors — we build APIs developers actually want to use. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI Observability: Stop Flying Blind in Production</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Wed, 27 May 2026 11:21:37 +0000</pubDate>
      <link>https://dev.to/qodors/ai-observability-stop-flying-blind-in-production-2i87</link>
      <guid>https://dev.to/qodors/ai-observability-stop-flying-blind-in-production-2i87</guid>
      <description>&lt;p&gt;You shipped your AI feature three months ago. Users love it. Usage is growing.&lt;/p&gt;

&lt;p&gt;But when someone asks "How's the AI performing?" — you have no idea.&lt;/p&gt;

&lt;p&gt;Is it answering correctly? How often does it fail? Which queries cost the most? When response times spike, what's the cause?&lt;/p&gt;

&lt;p&gt;Most teams can tell you their web server uptime down to the second. Ask them about their AI accuracy in production, and they just shrug.&lt;/p&gt;

&lt;p&gt;That's a problem.&lt;br&gt;
Why Traditional Monitoring Doesn't Work for AI&lt;/p&gt;

&lt;p&gt;Your standard observability stack tracks HTTP status codes, response times, error rates. That works fine for regular APIs.&lt;/p&gt;

&lt;p&gt;AI systems are different. A 200 response doesn't mean success. The model could return complete nonsense with perfect status codes.&lt;/p&gt;

&lt;p&gt;Traditional metrics miss what actually matters:&lt;/p&gt;

&lt;p&gt;Quality: Did the AI give a good answer or garbage?&lt;br&gt;
Cost: Was that response worth the API bill?&lt;br&gt;
Latency: Why did that query take 12 seconds?&lt;br&gt;
Accuracy: Is the system getting better or worse over time?&lt;/p&gt;

&lt;p&gt;You need AI-specific observability. Not just server monitoring with extra dashboards.&lt;br&gt;
What AI Observability Actually Looks Like&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Response Quality Scoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every AI response needs automatic quality assessment. Not manual review — that doesn't scale.&lt;/p&gt;

&lt;p&gt;Set up scoring pipelines that check:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Relevance: Does the answer match the question?
Completeness: Did it address all parts of the query?
Safety: Any inappropriate or harmful content?
Consistency: Same question, similar answer?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Use a separate model to grade responses. GPT-4 judging GPT-3.5 outputs. Claude evaluating your custom model results. Cross-validation prevents bias.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cost Per Interaction Tracking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Break down spending by feature, user segment, query type. Not just total monthly bills.&lt;/p&gt;

&lt;p&gt;Which features burn the most tokens? Which user behaviors trigger expensive operations? When costs spike, which workflows drove it?&lt;/p&gt;

&lt;p&gt;Track cost efficiency: dollars spent per successful interaction. If quality stays flat but costs double — something's broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Latency Breakdown&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI requests have multiple steps. Model inference, prompt processing, response formatting, any retrieval operations.&lt;/p&gt;

&lt;p&gt;Don't just measure total response time. Measure each component. When things slow down, you need to know if it's the model, your preprocessing, or network issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Failure Classification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI systems fail in unique ways. Model timeouts. Context window overflows. Safety filter blocks. Hallucination detection triggers.&lt;/p&gt;

&lt;p&gt;Traditional error monitoring lumps these together. You need granular failure categories. What broke? Why? How often? Which failures matter most to users?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Quality Drift Detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Models degrade over time. Data distribution shifts. User expectations evolve. Quality that was good six months ago might not be good today.&lt;/p&gt;

&lt;p&gt;Set up regression detection. Compare current performance to historical baselines. Alert when accuracy drops below thresholds.&lt;br&gt;
The Monitoring Stack That Works&lt;/p&gt;

&lt;p&gt;This isn't one tool. It's an architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging Layer&lt;/strong&gt;: Capture everything. Input, output, model used, tokens consumed, processing time, quality scores. Structure it for analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time Dashboards&lt;/strong&gt;: Quality metrics, cost trends, latency percentiles, failure rates. Not just pretty charts — actionable data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alerting System&lt;/strong&gt;: Quality below threshold? Costs spiking? Response times degrading? Alert the right people with context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analysis Tools&lt;/strong&gt;: Historical trends, A/B test results, user satisfaction correlation. What's working? What's not? Why?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Pipeline&lt;/strong&gt;: Move logs to your data warehouse. Enable deeper analysis. Feed insights back into model improvements.&lt;br&gt;
The Implementation Reality&lt;/p&gt;

&lt;p&gt;Most teams skip this entirely. They ship the feature and hope for the best.&lt;/p&gt;

&lt;p&gt;Others bolt on basic logging after problems surface. That's reactive firefighting, not observability.&lt;/p&gt;

&lt;p&gt;Smart teams build monitoring from day one. Every AI request gets instrumented. Every response gets evaluated. Every failure gets categorized.&lt;/p&gt;

&lt;p&gt;It takes discipline. Extra engineering time. More complex deploys.&lt;/p&gt;

&lt;p&gt;But when something goes wrong at 2 AM — and it will — you'll know exactly what, why, and how to fix it.&lt;br&gt;
The Questions Your Dashboard Should Answer&lt;/p&gt;

&lt;p&gt;If your AI observability can't tell you this, it's incomplete:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Right now: Is our AI feature healthy or degraded?
This week: Are we getting better or worse results than last week?
This month: Which improvements had real impact on user satisfaction?
Cost analysis: Where are we overspending? What optimizations worked?
Quality trends: Are users getting better answers over time?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you can't answer these questions with data — you're flying blind.&lt;br&gt;
Our Take&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=ai_observability" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, we build observability into AI systems from the start. Not as an afterthought when things break.&lt;/p&gt;

&lt;p&gt;Because shipping AI features is easy. Keeping them reliable, cost-effective, and improving over time — that's the hard part.&lt;/p&gt;

&lt;p&gt;Most teams optimize for the demo. We optimize for production. That includes knowing when production isn't working.&lt;br&gt;
If You're Running AI Features Without Proper Observability&lt;/p&gt;

&lt;p&gt;Five things to implement this week:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Start logging everything**. Input, output, cost, latency, quality scores. You can't improve what you don't measure.
**Set up automated quality assessment**. Use one model to grade another's responses. Scale manual review.
**Track cost per successful interaction**. Not just total bills. Where does money go for good vs bad outcomes?
**Define your quality metrics**. What makes a good AI response in your product? Measure that specifically.
**Build alerts for degradation**. Quality drops, costs spike, latency increases — know immediately.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Don't wait until your AI breaks in production to start measuring it.&lt;/p&gt;

&lt;p&gt;Build the visibility layer now. Because the alternative is explaining to users why you didn't know your AI was broken.&lt;/p&gt;

&lt;h1&gt;
  
  
  AIObservability #AIMonitoring #ProductionAI #AIEngineering #StartupCTO #AIArchitecture #MLOps #TechLeadership #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we build observable AI systems, not black boxes. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>monitoring</category>
      <category>mlops</category>
      <category>observability</category>
    </item>
    <item>
      <title>LLM Costs Are Killing Your Startup. Here's Your Cost Optimization Playbook.</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Fri, 22 May 2026 10:12:18 +0000</pubDate>
      <link>https://dev.to/qodors/llm-costs-are-killing-your-startup-heres-your-cost-optimization-playbook-1kh6</link>
      <guid>https://dev.to/qodors/llm-costs-are-killing-your-startup-heres-your-cost-optimization-playbook-1kh6</guid>
      <description>&lt;p&gt;Picture this: You launched an AI writing tool six months ago. Growing fast. Users love it.&lt;/p&gt;

&lt;p&gt;Then the bill comes. $14K from OpenAI. Last month it was $8K. The month before, $3K.&lt;/p&gt;

&lt;p&gt;Every user interaction hits GPT-4. Every document generated. Every revision suggested. Every grammar check. Your AI costs are growing faster than your revenue.&lt;/p&gt;

&lt;p&gt;This is happening to hundreds of AI startups right now.&lt;/p&gt;

&lt;p&gt;Why Your LLM Bill Is Out of Control&lt;/p&gt;

&lt;p&gt;Most teams treat API calls like they're free. Fire a request to GPT-4, get an answer, move on.&lt;/p&gt;

&lt;p&gt;That works fine when you have 50 users. When you hit 5,000 users making 10 AI requests each per day — you're looking at 50,000 API calls. At current pricing, that's real money.&lt;/p&gt;

&lt;p&gt;The hidden multiplier is repeat requests. Users ask the same questions. Generate similar content. Run identical workflows.&lt;/p&gt;

&lt;p&gt;You're paying OpenAI to compute the same answers over and over.&lt;/p&gt;

&lt;p&gt;The Cost Optimization Stack That Actually Works&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Semantic Caching (The Biggest Win)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Regular caching looks for exact matches. "What is machine learning?" gets cached. "What is ML?" misses the cache entirely.&lt;/p&gt;

&lt;p&gt;Semantic caching works differently. It converts questions into embeddings, then checks if a similar question was asked recently. If the embedding distance is close enough — serve the cached response.&lt;/p&gt;

&lt;p&gt;Smart AI products implement this early. Within two weeks, cache hit rates typically reach 60%. Six out of ten requests never touch OpenAI.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Response Compression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Most teams send the full conversation history with every request. A 10-turn conversation becomes a 5,000-token context window.&lt;/p&gt;

&lt;p&gt;Instead, summarize old context. Keep recent messages verbose, compress everything else into a 200-token summary. The model gets enough context to stay coherent without burning tokens on ancient chat history.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model Tiering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Not every request needs GPT-4. Simple classification, basic Q&amp;amp;A, formatting tasks — GPT-3.5 or Claude Haiku handle these fine at 10x lower cost.&lt;/p&gt;

&lt;p&gt;Build a routing layer. Intent classification happens first with a cheap model. Complex reasoning gets escalated to expensive models.&lt;/p&gt;

&lt;p&gt;80% of requests can stay on the cheap tier. Quality barely changes.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Batch Processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Individual API calls have overhead. Response time, connection setup, per-request pricing.&lt;/p&gt;

&lt;p&gt;For non-real-time workflows — content generation, data processing, analysis — batch everything. OpenAI's batch API costs 50% less than real-time calls.&lt;/p&gt;

&lt;p&gt;That background job that generates product descriptions? Perfect for batching.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt Compression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Shorter prompts cost less. Every token counts.&lt;/p&gt;

&lt;p&gt;Audit prompts regularly. Remove verbose examples. Cut unnecessary instructions. Use bullet points instead of paragraphs.&lt;/p&gt;

&lt;p&gt;A 1,200-token system prompt can often compress to 400 tokens without losing functionality. That's 66% cost reduction on every call.&lt;/p&gt;

&lt;p&gt;The Implementation Reality&lt;/p&gt;

&lt;p&gt;This isn't plug-and-play. You need architecture.&lt;/p&gt;

&lt;p&gt;Caching Layer: Redis or Postgres with vector similarity search. Not complicated, but it needs monitoring.&lt;/p&gt;

&lt;p&gt;Routing Logic: A lightweight service that decides which model handles each request. Rules-based or ML-based classification.&lt;/p&gt;

&lt;p&gt;Usage Monitoring: Track costs per feature, per user, per model. You can't optimize what you can't measure.&lt;/p&gt;

&lt;p&gt;Fallback Handling: What happens when the cache fails? When the cheap model can't handle a request? When OpenAI is down?&lt;/p&gt;

&lt;p&gt;The Numbers That Matter&lt;/p&gt;

&lt;p&gt;From typical implementations across AI products:&lt;/p&gt;

&lt;p&gt;• 60-80% cache hit rates on semantic caching after two weeks&lt;br&gt;
• 70% cost reduction with proper model tiering&lt;br&gt;
• 50% savings on batch-eligible workloads&lt;br&gt;
• Overall reduction: 65-85% without feature cuts&lt;/p&gt;

&lt;p&gt;The setup takes 2-4 weeks. The savings compound forever.&lt;/p&gt;

&lt;p&gt;Our Take&lt;/p&gt;

&lt;p&gt;Most startups can run production AI features for under $1,000/month. Even at scale. But you need the infrastructure layer that treats API calls like the expensive resource they are.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=llm_cost_optimization" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;  , these optimization patterns are built from day one. Because watching your AI bill triple every month isn't a scaling problem. It's an architecture problem.&lt;/p&gt;

&lt;p&gt;If You're Staring at a Five-Figure LLM Bill&lt;/p&gt;

&lt;p&gt;Five questions to ask your team:&lt;/p&gt;

&lt;p&gt;• Are you caching semantically similar requests? If not, you're burning 50-70% of your budget.&lt;br&gt;
• Does every request really need GPT-4? Simple tasks should route to cheaper models automatically.&lt;br&gt;
• Can any workflows be batched? Real-time isn't always necessary.&lt;br&gt;
• How long are your prompts? Every unnecessary token adds up across millions of calls.&lt;br&gt;
• Do you know your cost per feature? If you can't measure it, you can't fix it.&lt;/p&gt;

&lt;p&gt;Your AI features don't need to cost enterprise money. They need enterprise architecture.&lt;/p&gt;

&lt;p&gt;Build the cost layer. Don't let OpenAI pricing dictate your runway.&lt;/p&gt;

&lt;h1&gt;
  
  
  LLMCosts #AIOptimization #StartupFounders #OpenAI #CostEngineering #AIArchitecture #StartupCTO #TechDebt #QodorsEdge
&lt;/h1&gt;

&lt;p&gt;Written by the team at Qodors — we build cost-efficient AI systems, not budget-burning demos. → &lt;a href="//Written%20by%20the%20team%20at%20Qodors%20%E2%80%94%20we%20build%20cost-efficient%20AI%20systems,%20not%20budget-burning%20demos.%20%E2%86%92%20www.qodors.com"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>optimization</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI Wrappers Are Dying. Here's What Replaces Them.</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Tue, 19 May 2026 11:52:01 +0000</pubDate>
      <link>https://dev.to/qodors/ai-wrappers-are-dying-heres-what-replaces-them-3b63</link>
      <guid>https://dev.to/qodors/ai-wrappers-are-dying-heres-what-replaces-them-3b63</guid>
      <description>&lt;p&gt;Remember when every other startup on Product Hunt was "ChatGPT but for X"?&lt;/p&gt;

&lt;p&gt;That era is over.&lt;/p&gt;

&lt;p&gt;We watched it happen in real time. A client came to us last year with an AI writing tool. Nice UI. Clean onboarding. Under the hood? A single OpenAI API call with a system prompt.&lt;/p&gt;

&lt;p&gt;Three months later, OpenAI released custom GPTs. His entire product became a free feature overnight.&lt;/p&gt;

&lt;p&gt;He's not alone.&lt;/p&gt;

&lt;p&gt;What Happened to the Wrapper Economy&lt;/p&gt;

&lt;p&gt;The playbook was simple. Take an LLM API. Wrap a UI around it. Add a login page and a Stripe subscription. Ship it.&lt;/p&gt;

&lt;p&gt;For a while, it worked. Users didn't know how to use ChatGPT properly. They needed guided experiences. Wrappers filled that gap.&lt;/p&gt;

&lt;p&gt;Then the gap closed.&lt;/p&gt;

&lt;p&gt;OpenAI added custom instructions. Then custom GPTs. Then the GPT Store. Claude got Projects and system prompts that anyone can configure. Google shipped Gems.&lt;/p&gt;

&lt;p&gt;Every feature a wrapper offered became a native capability of the platform it depended on.&lt;/p&gt;

&lt;p&gt;The moat was never the UI. It was the user's ignorance. And that's not a moat. That's a countdown timer.&lt;/p&gt;

&lt;p&gt;Why Most Wrappers Can't Survive&lt;/p&gt;

&lt;p&gt;Three reasons. All fatal.&lt;/p&gt;

&lt;p&gt;**    1.Zero switching cost.**&lt;/p&gt;

&lt;p&gt;If your product is a prompt and an API call, the user can rebuild it in 10 minutes. With custom GPTs, they don't even need to code. They just describe what they want and get roughly the same thing your paid product offers.&lt;/p&gt;

&lt;p&gt;Your $29/month subscription is competing with free.&lt;/p&gt;

&lt;p&gt;**   2. Platform risk is absolute.**&lt;/p&gt;

&lt;p&gt;You don't control the model. You don't control the pricing. You don't control the features.&lt;/p&gt;

&lt;p&gt;OpenAI raises API costs? Your margins disappear. They ship a feature that overlaps with yours? Your value proposition disappears. They change their terms of service? Your business model disappears.&lt;/p&gt;

&lt;p&gt;Building a company entirely dependent on another company's API with zero contractual protection — that's not a startup. That's a hope.&lt;/p&gt;

&lt;p&gt;**    3.No defensible IP.**&lt;/p&gt;

&lt;p&gt;What do you actually own? A prompt template? A UI skin? A landing page?&lt;/p&gt;

&lt;p&gt;Investors are asking this question now. "What happens if OpenAI builds this?" If the honest answer is "we're done" — that's your answer.&lt;/p&gt;

&lt;p&gt;What's Actually Working Instead&lt;/p&gt;

&lt;p&gt;The AI products that survive share common traits. None of them are wrappers.&lt;/p&gt;

&lt;p&gt;They own their data layer.&lt;/p&gt;

&lt;p&gt;The product's value isn't the AI call. It's the proprietary data the AI operates on.&lt;/p&gt;

&lt;p&gt;A legal tech startup we worked with built a contract analysis tool. The AI is important, sure. But the real value is their database of 50,000+ annotated legal clauses built over 18 months. No custom GPT replaces that.&lt;/p&gt;

&lt;p&gt;Your data is your moat. Not your prompt.&lt;/p&gt;

&lt;p&gt;They solve workflow problems, not text problems.&lt;/p&gt;

&lt;p&gt;Wrappers generate text. Surviving products automate entire processes.&lt;/p&gt;

&lt;p&gt;There's a difference between "summarize this document" and "read this document, extract the 12 fields we need, validate against our rules, flag exceptions, update the database, and notify the right person."&lt;/p&gt;

&lt;p&gt;The first one is a wrapper. The second one is a product.&lt;/p&gt;

&lt;p&gt;They use AI as infrastructure, not interface.&lt;/p&gt;

&lt;p&gt;In the best AI products, users don't even know AI is involved. It runs in the background. Scoring, classifying, routing, deciding. The user sees results. Not a chat window.&lt;/p&gt;

&lt;p&gt;The moment your product feels like "talking to ChatGPT with extra steps" — you've already lost.&lt;/p&gt;

&lt;p&gt;They're model-agnostic.&lt;/p&gt;

&lt;p&gt;Smart teams build abstraction layers. They can swap GPT-4 for Claude for Gemini for an open-source model without rewriting their product.&lt;/p&gt;

&lt;p&gt;Because they know what we all know — today's best model is tomorrow's commodity. The product has to stand without any specific model propping it up.&lt;/p&gt;

&lt;p&gt;The Hard Truth About "AI Startups"&lt;/p&gt;

&lt;p&gt;Most AI startups aren't AI companies. They're UI companies with an API key.&lt;/p&gt;

&lt;p&gt;And that's fine for a side project. But if you're raising money, hiring a team, and betting your next two years on it — you need more than a wrapper.&lt;/p&gt;

&lt;p&gt;You need proprietary data. Or proprietary workflow logic. Or proprietary domain expertise baked into the system. Ideally all three.&lt;/p&gt;

&lt;p&gt;The AI layer should be replaceable. Your product shouldn't be.&lt;/p&gt;

&lt;p&gt;Our Take&lt;/p&gt;

&lt;p&gt;At Qodors (&lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=ai_wrappers_dying" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;), we've had founders come to us with wrapper ideas almost every week this past year. Our first question is always the same:&lt;/p&gt;

&lt;p&gt;"What happens when the model provider ships this as a native feature?"&lt;/p&gt;

&lt;p&gt;If there's no good answer, we don't build it. We help them find the version of the idea that actually survives.&lt;/p&gt;

&lt;p&gt;That usually means going deeper. Building the data pipeline. Designing the workflow engine. Creating the integration layer that connects AI to the messy, specific, real-world systems their users already depend on.&lt;/p&gt;

&lt;p&gt;That's harder than wrapping an API. It takes longer. Costs more upfront.&lt;/p&gt;

&lt;p&gt;But it's still standing 18 months later. The wrappers aren't.&lt;/p&gt;

&lt;p&gt;If You're Building an AI Product Right Now&lt;/p&gt;

&lt;p&gt;Honest questions to sit with:&lt;/p&gt;

&lt;p&gt;• Strip out the AI completely. Does your product still solve a problem? If not, you don't have a product. You have a demo.&lt;br&gt;
• Can a user replicate your core value with a custom GPT in 15 minutes? If yes, your pricing model has an expiration date.&lt;br&gt;
• What data do you own that nobody else has? If the answer is nothing — start there. The data strategy matters more than the model strategy.&lt;br&gt;
• Are you building on one model or building model-agnostic? Single model dependency is single point of failure.&lt;br&gt;
• Is AI your product or your infrastructure? The best answer is infrastructure. Always.&lt;/p&gt;

&lt;p&gt;The wrapper era taught us something useful. There's massive demand for AI-powered tools. People will pay for products that save them time and reduce decisions.&lt;/p&gt;

&lt;p&gt;But they won't pay for a middleman between them and ChatGPT. Not when ChatGPT keeps getting better at being ChatGPT.&lt;/p&gt;

&lt;p&gt;Build the thing the model can't become. That's your product.&lt;/p&gt;

&lt;p&gt;Written by the team at Qodors — an AI-first software studio that builds products, not wrappers. → &lt;a href="http://www.qodors.com" rel="noopener noreferrer"&gt;www.qodors.com&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AIStartups #AIWrappers #ProductStrategy #StartupFounders #AIIntegration #BuildSmart #TechStrategy #OpenAI #QodorsEdge
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
