Docker Compose Configuration

How to Build a Docker Compose Configuration That Scales Smoothly

Everything looks fine when you have one container. Add a second, and your Docker Configuration already starts drifting toward chaos — then come databases and caches, and the whole setup turns fragile. Ports collide, data disappears, services randomly fail to connect.

The problem isn’t Docker itself. It’s how people build and ignore their compose configs early on. Treat it like a temporary script — and you’ll get a system nobody wants to touch later.

The Structure You Can’t Ignore

At the core, every docker compose file is built on three things: services, volumes, and networks. Sounds simple — until you mix them incorrectly.

Services define containers. Volumes handle persistence. Networks control communication. Break that separation, and your config becomes unmaintainable.

services:
  web:
    image: nginx:1.25-alpine
    ports:
      - "8080:80"
    restart: unless-stopped

No junk, no legacy keys. And yes — no “version”. It’s deprecated, and keeping it just signals outdated practices in your docker compose configuration.

Where Most Engineers Mess Up

The real issues start with volumes and networks.

Anonymous volumes vanish on shutdown — that’s silent data loss waiting to happen. No explicit network setup leads to service discovery problems that are painful to debug later.

A proper docker compose yaml example always makes persistence and isolation explicit. Named volumes stay. Networks define who talks to whom. Anything else is guesswork.

Minimal Doesn’t Mean Naive

A good docker compose example isn’t the shortest one — it’s the one that reflects production intent.

If your container doesn’t need write access, don’t give it. If your ports are exposed, know exactly why. Small constraints here prevent big debugging sessions later.

The Multi-Container Reality Check

Things get interesting when you add a database.

Most developers rely on docker compose depends_on and assume it solves startup order. It doesn’t. It only waits for the container, not the service inside it. That’s how you get random connection failures on boot.

services:
  api:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy

db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s

Health checks are what actually fix race conditions. Without them, you’re just hoping timing works out — and that’s not how stable docker compose configurations are built.

Environment and Config Discipline

Handling docker compose env variables the wrong way is another classic mistake. Hardcoding secrets inside YAML is how configs leak and environments drift.

Use .env files. Keep configs clean. Swap environments without rewriting everything.

Also — never use latest tags. Pin versions. Every time. Non-deterministic builds are a slow-moving disaster.

Common Failures You Will Hit

  • broken YAML indentation
  • port already in use
  • missing networks
  • containers exiting instantly
  • services unable to connect

The fastest way to catch half of them — run docker compose config before deployment.

Where Compose Works — and Where It Doesn’t

Docker compose in production is fine — as long as you stay within its limits. Single host, a few services, predictable load — it works cleanly.

But once you need scaling across machines, load balancing, or orchestration — Compose stops being enough. That’s where Kubernetes comes in.

Bottom Line

A solid docker compose configuration isn’t about complexity — it’s about control.

If your setup is predictable, reproducible, and doesn’t rely on one person to understand it — you did it right. If not, it’s just a matter of time before it breaks.

How I Structure CLAUDE.md for Production Projects

If you have been using Claude Code for more than a week, you have almost certainly written a CLAUDE.md file. And if you are like most developers, your first version was not very good — mine was not, either.

The file probably read something like: “This is a TypeScript project. We use Express and Prisma. Follow best practices.”

That is not a CLAUDE.md. That is a README. And it produces the same result as no CLAUDE.md at all, because it tells the agent about the project without telling it how to behave.

This article is about the difference between those two things, and the structure I have settled on after iterating through a lot of versions.

The mental model shift

CLAUDE.md is not documentation. It is a behavioral contract.

Documentation describes what something is. A behavioral contract specifies what an agent must do, must not do, and should do by default in the absence of other instructions.

When you write “This is a TypeScript project using Prisma,” you are writing documentation. The agent might infer some behavioral rules from it — it might assume you want TypeScript syntax, might guess you want Prisma for database access — but it is guessing, and guesses are inconsistent.

When you write “All database access must go through the repository layer in /src/repositories. Never write Prisma queries directly in route handlers,” you have written a policy. The agent follows policies reliably. It guesses at implications inconsistently.

This distinction shapes everything.

The structure I use

Every CLAUDE.md I write now has six sections. Not all sections need to be long — some projects have two lines in a section, others have fifteen — but the structure is consistent.

1. Project identity (one paragraph maximum)

Not documentation. A one-sentence description of the project type and domain, followed by one sentence about the primary development mode: “This is a production API, not a prototype. Stability and test coverage take priority over velocity.”

That second sentence is the behavioral load-bearing one. It tells the agent how to weight tradeoffs throughout every session.

2. Always do

Explicit, specific behaviors the agent should apply without being asked. This is not a wishlist — every item must be something you actually want on every task, not just some tasks.

Examples from my current API project:

- After implementing any new function, run the test suite before reporting completion.
- Every new function needs at least one happy-path test and one error-case test.
- When modifying database-touching code, check for missing index usage before finishing.
- Log at the appropriate level: debug for development noise, info for business events, error for failures that need attention.

The specificity matters. “Always write tests” generates one trivial test. “Every new function needs at least one happy-path test and one error-case test, using Vitest, in a .test.ts file in the same directory as the source file” generates what you actually want.

3. Never do

This section has the highest leverage per line of anything in CLAUDE.md. Every time the agent does something that frustrates you — a pattern you always correct, a shortcut you always revert — write a rule here.

Over time, this list becomes your project’s permanent memory of what does not work.

Mine includes:

- Never modify committed migration files. Create a new migration instead.
- Never use `any` in TypeScript. If the type is genuinely unknown, use `unknown` and handle it.
- Never write inline styles. All styling goes through the design system tokens.
- Never hard-code environment-specific values. Use environment variables accessed through the config module.
- Never `console.log` in production code. Use the structured logger at `/src/lib/logger.ts`.
- Never declare a task done before running the test suite.

The last one is important. Without it, agents have a consistent bias toward reporting completion before verifying it.

4. Code style and conventions

This is where stack-specific rules live. Not a style guide — that belongs in ESLint or your linter config. This section covers the decisions that automated tools cannot enforce: naming patterns, module organization, error handling philosophy.

For a TypeScript API:

- Error handling: all errors should be typed. Throw typed errors, not plain Error objects.
- Repository functions return Result types (Ok/Err), not throw. Route handlers translate Results to HTTP responses.
- File naming: kebab-case for files, PascalCase for exported classes and types, camelCase for everything else.
- Module boundaries: `/services` contains business logic, `/repositories` contains data access, `/routes` contains HTTP handling. These do not import from each other laterally — data flows up through the stack.

5. Scope and tool permissions

What the agent is allowed to do, what requires approval, and what it must not do at all with its tool access.

- Shell commands that modify the database (migrations, seeds) require explicit approval before execution.
- Never delete files — move to a `.trash/` directory at the repo root instead.
- Changes to `/config/` files require explicit approval.
- Infrastructure changes (any file under `/terraform/`) are read-only — do not modify, only read to understand context.

The “never delete, move to trash” pattern is one I adopted after an agent deleted a file that turned out to be needed and was not in git. Moving to trash gives a one-step recovery.

6. Memory guidance

Instructions for what to write to persistent memory versus what should stay in CLAUDE.md.

- Write project-specific decisions and their rationale to `.claude/memory/project.md`.
- Write discovered patterns (API quirks, library behavior) to `.claude/memory/discoveries.md`.
- Do not write temporary task context to memory — only durable knowledge.

This prevents memory from becoming a graveyard of task notes while ensuring genuine discoveries persist across sessions.

Nested CLAUDE.md for monorepos

If you have a monorepo, a single root CLAUDE.md that tries to cover everything produces long, confused instructions. Claude Code supports nested files — a CLAUDE.md in a subdirectory extends or overrides the root for that scope.

My structure:

CLAUDE.md                    # Global conventions, security rules, commit format
apps/api/CLAUDE.md           # API-specific rules (error handling, test patterns)
apps/web/CLAUDE.md           # Frontend-specific rules (component conventions, CSS)
packages/shared/CLAUDE.md    # Library rules (public API discipline, no side effects)

Each child file is short and focused. The agent only loads what is relevant to its current working location.

The root file carries the rules that apply everywhere and that you never want to repeat — the security rules, the commit message format, the things that are never acceptable anywhere in the codebase.

Versioning and maintenance

Treat CLAUDE.md as code. Version it in git. Review changes in PRs. When you update a rule, add a comment explaining why — especially for “never do” items, where the reason is often a specific bad experience that is not obvious from the rule text.

I keep a # Changelog section at the bottom of each CLAUDE.md with the three most recent changes and their dates. It makes it easy to understand why a rule exists when you come back to it months later.

What I got wrong before getting this right

In roughly the order I learned them:

  1. Writing descriptions instead of policies
  2. One massive CLAUDE.md instead of a layered hierarchy
  3. Vague “always” instructions instead of specific ones
  4. Ignoring the “never do” section entirely
  5. Not versioning the file
  6. Forgetting to update it when I found a new agent anti-pattern
  7. Treating it as a one-time setup instead of a living document

The last one is the most important. The best CLAUDE.md files are the ones that get updated every time something goes wrong, until “going wrong” becomes rare.

Going deeper

This article covers Chapter 1 of a longer technical guide I published: Claude Code: The Production Guide. The full guide covers agent harnesses, parallelization with git worktrees, the auto-research framework, memory systems, MCP server security, and the complete security hardening model for running Claude Code with real shell and filesystem access.

If you are using Claude Code seriously and want the complete picture: Claude Code: The Production Guide ($29)

I also released a template pack with 15 ready-to-use CLAUDE.md configurations for specific stacks (Go, Python/FastAPI, Next.js, Terraform, dbt, and more) if you want a starting point rather than building from the structure above: CLAUDE.md Template Pack ($19)

What patterns have you found in CLAUDE.md that actually work? The comments are a good place for this — I find the design space genuinely interesting and there are almost certainly good patterns I have not tried.

AWS Vector Databases – Part 2: Search, Filtering, and Chunking

This is Part 2 of the AWS vector database series.

👉 Missed Part 1? Start here: Embeddings, Dimensions, and Similarity Search

In Part 1, we covered the fundamentals of embeddings and how similarity is measured. Now we move into how retrieval actually works in practice.

In this part, we’ll look at search patterns (KNN vs ANN), hybrid search, metadata filtering, and chunking strategies — the building blocks of effective RAG systems.

Vector Search Types

The following section explains the vector search types

Approach How it works When to use AWS services
KNN — Exact Nearest Neighbor Search Check every single item, compare it to your query, return the best matches. Perfectly accurate, but slow. Small datasets (under 100K vectors) or situations where you absolutely cannot afford to miss a result — like medical diagnostics or legal compliance checks. All vector services support KNN as a fallback, but it’s not practical at scale.
ANN — Approximate Nearest Neighbor Search Uses a smart index structure (graph or cluster) to find very likely nearest neighbors without checking everything Almost everything in production. If you’re building a RAG chatbot, semantic search, or recommendation engine, this is your default. OpenSearch Serverless, Aurora pgvector, MemoryDB, ElastiCache Valkey, DocumentDB, S3 Vectors, Neptune Analytics.

Hybrid Search

Hybrid search runs two searches at the same time—one that understands meaning (vector search) and one that looks for exact words (keyword search)—and then combines the results.

For example, a user might search: “lambda timeout issue nodejs.”

  • The Vector Search understands the intent (performance/debugging)
  • The Keyword Search ensures exact terms like lambda and nodejs are matched.

Note: The scoring method used to combine these two result sets is called Reciprocal Rank Fusion (RRF). It doesn’t simply add scores—it prioritizes documents that rank highly in both searches. For example, if a document ranks #1 in keyword search and #2 in vector search, RRF will push it to the top of the final results.

This is especially useful for enterprise RAG. Users rarely search with purely natural language or purely exact keywords—they usually mix both.

Service Implementation
OpenSearch Serverless Supports Native (RRF). The most robust option; its “Neural Search” feature handles the hybrid merging automatically.
Aurora pgvector This is sql based and best for relational data; you manually combine tsvector (keywords) and vector (meaning) in one query.

Metadata Filtering

Metadata filtering narrows down results using structured data like date, category, or user ID—before or after the vector search runs.

Think of it like this: a vector search finds books similar to Harry Potter. But you only want books published after 2010 and available in English. Metadata filtering ensures you don’t waste time on the wrong results.

Pre-filtering vs Post-filtering

Approach How it works Trade-offs
Pre-filtering Applies filters first, then runs vector search on the remaining data Accurate and secure, but can be slower depending on the engine
Post-filtering Runs vector search first, then filters the results Fast, but may return zero results if none match the filters

Note: S3 Vectors uses a technique called tandem filtering, where metadata filters are applied during the vector search itself. This combines the accuracy of pre-filtering with the performance of post-filtering, making it well-suited for very large datasets.

Chunking

Chunking is simply breaking a long document into smaller, meaningful pieces before creating embeddings. If your chunks are too small, you lose context. If they’re too big, the important meaning gets buried in noise. The goal is to find the right balance.

Common Chunking Strategies

Strategy How it works Chunk size Best for
Fixed-size Split every N tokens/characters with optional overlap 256–512 tokens Simple content like logs or short descriptions
Recursive Split by paragraphs → sentences → words while preserving structure Variable General-purpose text (default in most tools)
Semantic Use an embedding model to split based on topic boundaries Variable Long-form content like whitepapers or legal docs
Document-structure Split using headings, sections, or document layout Variable Structured docs like manuals, HTML, or code
Sentence-window Store sentences, return surrounding context at query time 1 sentence (store) / window (return) High-precision Q&A

Bedrock Chunking Options

Bedrock option What it does Equivalent concept
Default ~300-token chunks that respect sentence boundaries Recursive (baseline)
Fixed-size You control chunk size and overlap Fixed-size
Hierarchical Searches small chunks but returns larger context Sentence-window
Semantic Splits based on topic boundaries Semantic
None No splitting — entire file treated as one chunk Document-structure (manual)

👉 Continue reading: In Part 3, we’ll compare AWS vector database options and build a practical decision framework to help you choose the right one.

AWS Vector Databases – Part 3 : Choosing the Right Vector Database on AWS

This is where everything comes together.

👉 In case you missed it:

By now, you understand the fundamentals and how retrieval works. The real question is:

Summary Comparison

Service Model Strength Scaling Bedrock KB
OpenSearch Serverless Distributed Full-text + semantic search Automatic (OCU) Yes
Aurora pgvector Relational SQL + vector hybrid queries Serverless v2 Yes
S3 Vectors Object storage Massive scale, lowest cost Fully serverless Yes
MemoryDB In-memory Ultra-low latency Cluster-based No
Valkey (ElastiCache) In-memory Semantic caching Cluster-based No
Neptune Analytics Graph Relationships + vectors Automatic Yes
DocumentDB Document JSON + vector workloads Instance-based Yes
Kendra Managed Enterprise search Fully managed N/A

Decision Matrix

Scenario Recommended Alternative
General-purpose RAG OpenSearch Serverless Aurora pgvector
Existing PostgreSQL app Aurora pgvector DocumentDB
Ultra-low latency (real-time) MemoryDB Valkey
Reduce LLM cost (caching) Valkey MemoryDB
Knowledge graph + vectors Neptune OpenSearch
Massive scale (billions) S3 Vectors OpenSearch
Enterprise search (managed) Kendra Bedrock KB + S3
Agent memory (short-term) Valkey / MemoryDB S3 Vectors

Cost Reality (2026)

Service Model Key Driver
S3 Vectors Storage + query Cheapest at scale
Aurora pgvector Instance + storage Cheaper if shared DB
OpenSearch Serverless OCU hours Minimum baseline cost
DocumentDB Instance + I/O I/O adds up
MemoryDB / Valkey In-memory RAM is expensive
Kendra Flat fee Enterprise pricing

Graviton5 has reduced compute costs by ~20%, but memory-heavy systems still cost the most.

When NOT to Use a Vector Database

Before building anything, check this:

  • Small dataset (<10K) → use in-memory search
  • Exact match queries → use SQL or search index
  • Structured filtering only → use a database
  • Static FAQ → use a lookup table
  • Real-time transactions → use relational/NoSQL DB
  • Near-zero budget → use FAISS + S3

That’s a wrap on the series. If you’re building with RAG or semantic search on AWS, you now have both the conceptual foundation and practical guidance to choose the right architecture.

Centralized Documentation Needed for Oxlint and Oxfmt Compatibility with Frameworks and File Types

Introduction & Problem Statement

Imagine you’re a developer tasked with integrating Oxlint and Oxfmt into a complex project. You’re halfway through configuring these tools when you hit a wall: Does Oxlint support React Native? Can Oxfmt handle TypeScript files with JSX? Without a centralized resource, you’re forced to scour forums, GitHub issues, or trial-and-error—a process that wastes hours and erodes trust in the Oxc ecosystem. This scenario wasn’t hypothetical; it was the norm until recently, when the Oxc documentation finally introduced a compatibility overview page. But why did this gap exist, and what does its resolution mean for developers?

The Root of the Problem: Fragmented Information, Amplified Complexity

The issue wasn’t just a lack of documentation—it was the decentralized nature of what existed. Compatibility details were scattered across README files, GitHub discussions, and community threads. This fragmentation created a mechanical inefficiency: developers had to assemble a puzzle of information, often leading to misconfigurations (e.g., using Oxlint with an unsupported framework) or abandoned workflows due to uncertainty. The growing complexity of the Oxc ecosystem—with its expanding list of frameworks and file types—exacerbated this, as each new addition introduced uncertainty multipliers.

For instance, a developer working with Vue 3 might assume Oxfmt supports its single-file components (.vue files) based on its TypeScript compatibility. Without explicit documentation, this assumption could lead to runtime errors or formatting inconsistencies, as the tool silently fails to parse Vue-specific syntax. The causal chain here is clear: ambiguity → incorrect assumptions → tool misuse → project delays.

Why Centralization Matters: A Mechanical Analogy

Think of Oxlint and Oxfmt as gears in a machine, and frameworks/file types as the cogs they must mesh with. Without a clear blueprint (centralized documentation), the gears risk grinding against incompatible cogs, causing friction (developer frustration) and wear (wasted time). The new compatibility page acts as a precision-engineered diagram, ensuring each gear fits its intended cogs. This isn’t just about convenience—it’s about preventing mechanical failure in the development workflow.

Edge Cases and Risk Mechanisms

Consider a developer using Oxlint with Svelte, a framework not yet listed on the compatibility page. Without explicit “unsupported” status, they might proceed, only to encounter false positives in linting or syntax misinterpretation. The risk here forms via a feedback loop: lack of documentation → user experimentation → undetected incompatibility → project bugs. Centralization breaks this loop by providing a stopgap: if Svelte isn’t listed, it’s a red flag, not an invitation to guess.

The Optimal Solution: Centralization Over Decentralization

While alternatives like community-maintained wikis or auto-generated compatibility matrices were considered, they fall short. Wikis suffer from inconsistency and outdatedness, while auto-generated matrices lack nuanced explanations (e.g., “partially supported” vs. “fully supported”). Centralized, manually curated documentation is optimal because it:

  • Ensures accuracy: Maintained by core contributors, reducing misinformation.
  • Provides context: Explains edge cases (e.g., “TypeScript 4.5+ required for feature X”).
  • Future-proofs: Easily updated as frameworks evolve or new file types emerge.

However, this solution has a breaking point: if updates lag behind tool releases, the documentation becomes stale, reverting the ecosystem to its pre-centralized chaos. The rule here is clear: If documentation isn’t synchronized with tool development → centralization fails.

Conclusion: A Blueprint for Trust

The addition of a centralized compatibility overview isn’t just a documentation update—it’s a structural reinforcement of the Oxc ecosystem. By eliminating ambiguity, it transforms Oxlint and Oxfmt from tools with potential into reliable components of developer workflows. The lesson? In technical ecosystems, clarity isn’t a luxury—it’s the load-bearing beam that prevents the entire structure from collapsing under the weight of complexity.

Compatibility Analysis: Oxlint and Oxfmt in the Wild

The Oxc ecosystem, like a finely tuned machine, relies on precise compatibility between its tools and the frameworks they interact with. Oxlint and Oxfmt, as critical cogs in this machine, must mesh seamlessly with various file types and frameworks to avoid mechanical failure in developer workflows. The recent addition of a centralized compatibility overview acts as a precision-engineered diagram, ensuring these tools function as intended. Here, we dissect six key scenarios, exposing the mechanisms of compatibility, the risks of misalignment, and the workarounds that keep the system running.

Scenario 1: React Ecosystem

File Types: .jsx, .tsx

Compatibility: Fully supported by both Oxlint and Oxfmt.

Mechanism: React’s component-based architecture aligns with Oxlint’s parsing logic, allowing it to detect syntax errors and enforce coding standards without deforming the structure of .jsx and .tsx files. Oxfmt’s formatter expands readability by aligning props and state variables according to React’s conventions.

Risk: Misconfigured React versions (e.g., React 18 with experimental features) can cause Oxlint to overheat—flagging false positives due to unrecognized syntax. This heats up the debugging process, wasting developer time.

Workaround: Explicitly specify React version in configuration files to prevent misinterpretation.

Scenario 2: Vue 3 Projects

File Types: .vue

Compatibility: Partially supported. Oxlint handles `

Recommendations & Best Practices for Oxlint and Oxfmt Compatibility

The introduction of a centralized compatibility overview for Oxlint and Oxfmt in the Oxc documentation is a precision-engineered solution to a problem that acted like a friction point in developer workflows. Here’s how to leverage this resource effectively, avoid common pitfalls, and ensure these tools function as reliable gears in your ecosystem.

1. Framework-Specific Configuration: Aligning Tools with Ecosystem Mechanics

Think of Oxlint and Oxfmt as calibrated instruments—their effectiveness depends on alignment with the framework’s structural mechanics. Misalignment causes mechanical failure in workflows:

  • React Projects (.jsx, .tsx): Oxlint’s parsing logic meshes with React’s component architecture, but version mismatches (e.g., React 18 experimental features) act like foreign particles in a machine, causing false positives.

    • Mechanism: Unrecognized syntax → parsing errors → flagged false positives.
    • Solution: Explicitly specify React version in configuration files (e.g., oxlint.config.json) to re-calibrate the tool.
  • Vue 3 Projects (.vue): Oxlint partially engages with Vue’s

Conclusion & Future Outlook

The introduction of a centralized compatibility overview for Oxlint and Oxfmt in the Oxc documentation marks a pivotal shift from fragmented guesswork to precision-engineered clarity. This move addresses a critical pain point: developers previously navigated a maze of READMEs, GitHub issues, and forums, often leading to misconfigurations and project delays. For instance, assumptions about Vue 3 .vue file support resulted in runtime errors due to Oxlint’s incomplete parsing of Vue-specific syntax in templates and styles. The causal chain is clear: ambiguity → incorrect assumptions → tool misuse → delays.

Centralized documentation acts as a load-bearing beam in the Oxc ecosystem, preventing its collapse under growing complexity. By explicitly listing supported frameworks and file types, it eliminates mechanical inefficiency—developers no longer waste time puzzle-solving for compatibility information. For example, React projects now have a clear mechanism to avoid false positives: explicitly specify the React version in oxlint.config.json. This alignment ensures Oxlint’s parsing logic meshes seamlessly with React’s component architecture, akin to gears interlocking with cogs in a machine.

However, edge cases remain. Unlisted frameworks like Svelte risk false positives or syntax misinterpretation without an explicit “unsupported” status. This is a mechanical failure point: the documentation must synchronize with tool releases to avoid staleness. If the documentation lags, it becomes a rusted gear, causing friction in workflows.

Looking ahead, stakeholders must prioritize manual curation of this documentation. Automated solutions risk missing context—such as version requirements or edge cases—that only human maintainers can provide. The optimal solution is a manually curated, centralized resource maintained by core contributors. This ensures accuracy, context, and future-proofing. A rule of thumb: If X (ecosystem complexity increases) → use Y (centralized, manually curated documentation).

In conclusion, centralized documentation transforms Oxlint and Oxfmt from unreliable tools into trusted components of the Oxc ecosystem. Clarity is not a luxury—it’s a necessity. Without it, the ecosystem risks mechanical failure under its own weight. By addressing this gap, the Oxc team has not only improved developer productivity but also reinforced trust in their tools. The future demands vigilance: keep the documentation synchronized, address edge cases, and ensure it remains a precision-engineered diagram for seamless tool-framework compatibility.

AWS Vector Databases Part 1: Embeddings, Dimensions & Similarity

This is Part 1 of a series exploring vector databases on AWS.

We recently evaluated multiple AWS vector database options to understand their trade-offs, performance characteristics, and real-world use cases. Before comparing services, it’s important to understand the core concepts that power vector search.

In this part, we’ll cover embeddings, dimensions, and similarity search — the foundation of every RAG and semantic search system.

What Are Embeddings?

Let’s say you’re building a customer support chatbot.

A user asks: “How do I change my login info?”
Your FAQ has: “Resetting your password.”

A keyword search might miss this. But as humans, we know they mean the same thing. That’s the gap embeddings solve.

An embedding is a numerical representation of content (text, image, code) where similar meaning leads to similar numbers. So even if the words differ, the intent stays close.

How Embeddings Are Created

Here’s what happens under the hood when you pass a sentence to an embedding model:

"How do I reset my password?"
        │
        ▼
   Tokenization         →  ["How", "do", "I", "reset", "my", "password", "?"]
        │
        ▼
   Embedding Model       →  Neural network (e.g., Titan v2)
        │
        ▼
   Vector Output         →  [0.021, -0.438, 0.712, ..., 0.155]   (1,536 floats)

The important part isn’t the numbers themselves—it’s that similar sentences produce vectors that are close to each other.

On AWS, you can generate embeddings using models like:

  • Titan Text Embeddings V2
  • Titan Embeddings G1 – Text
  • Embed 3
  • Embed Multilingual v3

If you’re getting started, Amazon Titan Embeddings V2 is a solid default—simple, cost-effective, and good enough for most use cases.

Note: The model you use for ingestion (storing data) must be the exact same model you use for inference (querying). If you embed your database using Amazon Titan but try to query it using an OpenAI embedding, the math won’t align, and your search results will be complete gibberish.

Dimensions

So far, we’ve seen that embeddings are just lists of numbers representing meaning. The next question is: how many numbers are in that list? That’s where dimensions come in.

A dimension is simply the number of values in an embedding list (vector).

For example:
[0.12, -0.45, 0.78, …, 0.33] → 1,536 values, a vector with 1,536 values has 1,536 dimensions

Different models produce different dimensions:

  • Cohere Embed v3 → 1,024
  • Amazon Titan Embeddings → 1,024 (default), 512, 256

Note: Historically, more dimensions meant better accuracy but higher storage costs and slower searches. However, the game changed with Amazon Titan Text Embeddings V2.Titan v2 supports “flexible” dimensions. You can generate a 1024-dimension vector and “truncate” it down to 512 or 256.

  • 1024 Dimensions: Maximum “nuance” and accuracy.
  • 256 Dimensions: Up to 4x less storage cost and faster search speeds, with only a marginal hit to accuracy.

Distance Metrics: Measure Similarity

Once you have thousands of embeddings stored, the database uses a distance metric to find the “nearest neighbors” to your query.

Metric Logic Use Case
Cosine Angle between vectors The Standard. Best for text and RAG. Ignores document length.
Euclidean (L2) Straight-line distance Best for Images or fixed-size data where magnitude matters.
Inner Product Direction + Magnitude Best for Recommendations where popularity or “weight” matters.

Why Metrics Matter: The “Wordiness” Problem

Let’s compare two vectors representing the same topic:

  • Vector A: [1, 2, 3] (A short, concise help article)
  • Vector B: [10, 20, 30] (A very long, detailed whitepaper on the same topic)

Even though they cover the exact same intent, different metrics interpret them wildly differently:

  • Cosine Similarity (The Compass): Sees that both arrows point at the exact same target in space. It gives them a perfect match score. This is why it’s the standard for RAG—you want your short question to match a long document.
  • Euclidean Distance (The Ruler): Measures the physical distance between the “tips” of the arrows. Because Vector B is so much longer, the ruler sees them as miles apart and may treat them as unrelated.
  • Inner Product (The Spotlight): It sees that both point the same way, but it gives Vector B a “higher” score because it is stronger/longer. This is perfect for recommendation engines where you want to highlight “heavy-hitting” content.

For example,

  • Query A: “How do I reset my password?”
  • Doc B: “A guide to password resets for new users.”

They mean the same thing, but Doc B is 1,000 words long.

  • Cosine correctly identifies them as a match because it ignores the extra “fluff” and focuses on the intent.
  • Euclidean might fail because the sheer volume of words in Doc B pushes its vector too far away from the short query.

Key Takeaway: For 95% of AWS text-based applications (Chatbots, Q&A, Knowledge Bases), use Cosine Similarity. It is the default in Aurora pgvector, OpenSearch, and S3 Vectors for a reason: it focuses on meaning over length.

👉 Continue reading: In Part 2, we’ll explore vector search patterns (KNN vs ANN), hybrid search, metadata filtering, and chunking — and how they impact real-world systems.

The Pain of Neglecting Maintainability in Code

I recently spent hours debugging a particularly nasty issue in a codebase where I had cut corners to meet a tight deadline. What should have been a simple fix turned into a frustrating ordeal because the code was poorly structured and lacked proper documentation. I finally resolved the issue, but not before learning a hard lesson about the importance of writing maintainable code from the start.

In my haste, I had duplicated logic in multiple places, used vague variable names, and failed to add comments explaining complex sections. This made the codebase confusing and difficult for me to navigate when troubleshooting. I realized that taking a bit of extra time upfront to write clean, well-structured code pays dividends down the line in saved time and reduced frustration.

Three Threads, One Terminal: Concurrency in Rust with Channels and Atomics

This is part of a series of posts about building b.roll, a terminal session recorder and search tool written in Rust. If you’re just joining, here are the previous posts:

  1. Building a 12-Command CLI in 120 Lines with clap Derive Macros
  2. Spawning a PTY in Rust: How broll Captures Your Terminal Without You Noticing

When broll records a terminal session, three things need to happen at the same time: keystrokes must flow into the shell, the shell’s output must appear on screen, and that output must be processed and stored in a database. Doing all three sequentially would mean dropped keystrokes and laggy rendering. So broll uses three threads, coordinated through channels and atomics.

This post walks through the concurrency architecture of broll’s recorder, explaining each primitive: std::thread::spawn, mpsc channels, Arc, AtomicBool, AtomicU16, Ordering, move closures, and signal_hook for terminal resize handling. All code is from src/recorder/mod.rs.

The Three-Thread Architecture

Here is the layout:

                    +------------------+
 Real stdin ------> | Thread 1 (stdin) | ------> PTY master (writer)
                    +------------------+

                    +------------------+
 PTY master ------> | Main thread      | ------> Real stdout
   (reader)         | (PTY -> stdout)  | ------> mpsc channel (tx)
                    +------------------+

                    +------------------+
 mpsc channel ----> | Thread 2         | ------> SQLite (via Database)
   (rx)             | (storage)        |
                    +------------------+

Thread 1 (stdin) reads from the real terminal and writes to the PTY master, forwarding your keystrokes to the sub-shell.

Main thread reads from the PTY master and writes to the real terminal, displaying the shell’s output. It also sends a copy of every chunk through an mpsc channel.

Thread 2 (storage) receives data from the channel, parses OSC markers, renders output through a VT100 virtual terminal, and stores the result in SQLite.

Spawning Threads with move Closures

Rust’s std::thread::spawn takes a closure and runs it on a new OS thread. The catch: the closure must own everything it uses. Rust enforces this because threads can outlive the scope that created them, and borrowing data across thread boundaries would create data races.

The move keyword transfers ownership of captured variables into the closure. Here is the stdin thread:

let mut pty_writer = writer;
let _stdin_handle = std::thread::spawn(move || {
    let mut stdin = std::io::stdin();
    let mut buf = [0u8; 1024];
    loop {
        match stdin.read(&mut buf) {
            Ok(0) | Err(_) => break,
            Ok(n) => {
                if pty_writer.write_all(&buf[..n]).is_err() {
                    break;
                }
            }
        }
    }
});

Without move, the compiler would reject this code. The closure captures pty_writer, and Rust cannot guarantee the closure will not outlive the variable. With move, ownership of pty_writer transfers into the closure. The original scope can no longer access it, which eliminates any possibility of concurrent access.

The pattern Ok(0) | Err(_) => break handles both clean EOF and I/O errors in one arm. When stdin closes or the PTY writer breaks, the thread exits cleanly.

mpsc Channels: One Producer, One Consumer

The main thread and the storage thread communicate through an mpsc (multiple-producer, single-consumer) channel:

let (tx, rx) = mpsc::channel::<Vec<u8>>();

The main thread holds tx (the sender). The storage thread holds rx (the receiver). Each time the main thread reads a chunk from the PTY, it sends a copy through the channel:

// Main thread: PTY -> stdout + channel
match reader.read(&mut buf) {
    Ok(0) => break,
    Ok(n) => {
        let raw = &buf[..n];
        stdout.write_all(raw)?;
        stdout.flush()?;
        let _ = tx.send(raw.to_vec());
    }
    Err(_) => break,
}

The raw.to_vec() call creates an owned copy of the bytes. The main thread cannot send a reference because the buffer will be reused on the next read. The let _ = discards any send error, which only occurs if the receiver has been dropped (meaning the storage thread has exited).

On the receiving side, the storage thread uses recv_timeout instead of a blocking recv:

match rx.recv_timeout(INCOMPLETE_LINE_TIMEOUT) {
    Ok(data) => {
        // Process the data, parse markers, store chunks
    }
    Err(mpsc::RecvTimeoutError::Timeout) => {
        // Flush accumulated output that has not been terminated by a marker
    }
    Err(mpsc::RecvTimeoutError::Disconnected) => {
        // Channel closed, flush remaining data and exit
        break;
    }
}

The timeout (2 seconds) serves a practical purpose. Some commands produce output without a trailing newline or marker. Without a timeout, that partial output would sit in the buffer indefinitely. The timeout flushes it to the database so it does not get lost.

When the main thread finishes its loop, it drops tx:

drop(tx);

This causes the storage thread’s next recv_timeout to return Disconnected, triggering a clean shutdown. This is the graceful teardown pattern for channel-based architectures: dropping the sender signals the receiver.

Arc: Shared Ownership Across Threads

Some state needs to be read by multiple threads. Rust’s ownership model does not allow two threads to own the same value. Arc (Atomic Reference Counting) provides a solution: it wraps a value in a heap-allocated container with a thread-safe reference count. Cloning an Arc increments the count; dropping one decrements it. The inner value is freed when the count hits zero.

broll uses Arc for two things: the resize flag and the terminal column width.

let resize_flag = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(signal_hook::consts::SIGWINCH, Arc::clone(&resize_flag))?;

let term_cols = Arc::new(AtomicU16::new(cols));
let term_cols_storage = Arc::clone(&term_cols);

Arc::clone does not clone the inner data. It creates a new pointer to the same allocation and increments the reference count. This is cheap: just an atomic increment.

Why Arc and Not Rc?

Rust has Rc (Reference Counting) for single-threaded shared ownership. It is slightly faster because it uses non-atomic operations. But Rc is not Send, meaning Rust will refuse to compile any code that tries to move an Rc to another thread. Arc uses atomic operations for its reference count, making it safe to share across threads. The compiler enforces this distinction at the type level.

Atomics: Lock-Free Shared State

Inside each Arc, broll uses atomic types rather than a Mutex. Atomics provide lock-free reads and writes for primitive values. No thread ever blocks waiting for another thread to release a lock.

AtomicBool for the Resize Flag

Terminal resize events arrive as SIGWINCH signals. The signal_hook crate registers a handler that sets an AtomicBool to true when the signal fires:

let resize_flag = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(signal_hook::consts::SIGWINCH, Arc::clone(&resize_flag))?;

The main thread checks the flag on every loop iteration using swap:

if resize_flag.swap(false, Ordering::Relaxed)
    && let Ok((cols, rows)) = crossterm::terminal::size()
{
    term_cols.store(cols, Ordering::Relaxed);
    let _ = pair.master.resize(PtySize {
        rows,
        cols,
        pixel_width: 0,
        pixel_height: 0,
    });
}

swap(false, Ordering::Relaxed) atomically reads the current value and replaces it with false, returning the old value. If it was true, a resize happened. This pattern ensures that even if multiple SIGWINCH signals arrive between checks, the resize logic runs exactly once per check.

AtomicU16 for Terminal Width

The storage thread needs the current terminal width to render output correctly through its VT100 parser. The main thread updates this value on resize:

term_cols.store(cols, Ordering::Relaxed);

The storage thread reads it when rendering:

let rendered = render_vt(&cmd_output_bytes, term_cols_storage.load(Ordering::Relaxed));

What Does Ordering::Relaxed Mean?

Atomic operations take a memory ordering parameter that controls how they interact with surrounding memory operations. Ordering::Relaxed provides the weakest guarantee: the atomic operation itself is atomic, but it places no constraints on the ordering of other reads and writes.

For broll’s use case, this is sufficient. The resize flag is a simple boolean signal where a missed update just means the resize happens on the next loop iteration. The column width is a hint for rendering, where using a slightly stale value produces output that is one render cycle behind. Neither case requires the stronger guarantees of Ordering::SeqCst or Ordering::Acquire/Release.

Signal Handling with signal_hook

Unix signals are tricky in any language. Signal handlers run in an interrupt context with severe restrictions on what they can do. The signal_hook crate provides a safe interface: instead of running arbitrary code in the handler, it simply sets an atomic flag.

signal_hook::flag::register(signal_hook::consts::SIGWINCH, Arc::clone(&resize_flag))?;

This registers a handler for SIGWINCH (window size change) that atomically sets resize_flag to true. The main thread polls this flag in its event loop. This is safe because the signal handler only performs a single atomic store, which is async-signal-safe.

Shutdown Sequence

The cleanup order matters:

drop(tx);                         // 1. Close the channel
let _ = child.wait();             // 2. Wait for the shell to exit
drop(_raw_guard);                 // 3. Restore terminal mode
let _ = storage_handle.join();    // 4. Wait for storage to finish
db.end_session(&session_id)?;     // 5. Mark session as ended

Dropping tx signals the storage thread to flush and exit. The RawModeGuard is dropped before the final message so that eprintln! renders correctly. The storage thread is joined to ensure all data reaches SQLite before the session is marked as ended.

Note that the stdin thread is deliberately not joined. It blocks on stdin.read(), which cannot be interrupted on all platforms. Since the process is about to exit anyway, the OS will clean it up.

A Standalone Example

Here is a minimal program that demonstrates two threads sharing an Arc<AtomicBool>:

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

fn main() {
    let flag = Arc::new(AtomicBool::new(false));

    // Spawn a worker thread that checks the flag
    let worker_flag = Arc::clone(&flag);
    let handle = thread::spawn(move || {
        println!("Worker: waiting for signal...");
        loop {
            if worker_flag.load(Ordering::Relaxed) {
                println!("Worker: signal received, shutting down.");
                break;
            }
            thread::sleep(Duration::from_millis(100));
        }
    });

    // Main thread does some work, then signals the worker
    thread::sleep(Duration::from_secs(1));
    println!("Main: sending signal.");
    flag.store(true, Ordering::Relaxed);

    handle.join().unwrap();
    println!("Main: worker exited cleanly.");
}

This is the same pattern broll uses for SIGWINCH. Replace thread::sleep with a signal handler, and you have the real architecture.

Why Not async/await?

broll uses OS threads rather than async runtimes like Tokio. The reasons are practical:

  1. Blocking I/O is unavoidable. stdin.read() and the PTY reader are blocking calls. An async runtime would need spawn_blocking for these, adding complexity without benefit.
  2. Three threads is a fixed, small number. There is no need for the thousands-of-tasks scalability that async provides.
  3. No additional dependencies. std::thread and std::sync are in the standard library. No runtime, no executor, no pinning.

When your concurrency model is “a few long-lived threads passing messages,” the standard library gives you everything you need.

Key Takeaways

  1. move closures transfer ownership into threads. This is not optional: the compiler requires it to prevent data races.
  2. mpsc channels provide typed, safe communication. Dropping the sender signals the receiver, enabling clean shutdown.
  3. Arc enables shared ownership across threads. It is Rc with atomic reference counting, and the compiler enforces that you use the right one.
  4. Atomics provide lock-free access to simple values. Ordering::Relaxed is often sufficient for flags and counters where eventual consistency is acceptable.
  5. signal_hook makes Unix signals safe. Instead of running complex code in a signal handler, set a flag and check it in your event loop.
  6. Explicit drop ordering controls shutdown. Channel close, child wait, guard drop, thread join: the sequence determines correctness.

broll’s entire concurrency model fits in one file, uses no external runtime, and relies on the compiler to verify thread safety at compile time. That is the Rust value proposition for systems programming: fearless concurrency is not a slogan, it is enforced by the type system.

Try it out:

Smart Home Security Is a $10 Billion Scam — Here’s What Actually Stops Burglars

The smart home security industry sells fear and subscriptions. After researching real burglary data, testing DIY systems, and talking to actual cops, here’s what really protects your home — and what’s just expensive theater.

The Dirty Secret: Professional Monitoring Is Mostly Theater

The average police response time to a residential burglar alarm is 7–15 minutes. In many cities, it’s longer. The average residential burglary takes 8–12 minutes (FBI UCR data, Eurostat crime surveys). Most burglars are in and out in under 10.

So what does professional monitoring buy you? A phone call. Someone at a call center sees an alert, tries calling you, waits for you to not answer, then calls the police. By the time a patrol car rolls up, the burglar is three neighborhoods away.

It gets worse. False alarm rates for residential security systems run between 94% and 98% (International Association of Chiefs of Police). Police departments have learned to deprioritize alarm calls. Many US cities now fine homeowners for repeated false alarms — $50 for the first, up to $500 for repeat offenders. Some departments have stopped responding to unverified alarms entirely.

You’re paying €30/month for the privilege of being deprioritized by emergency services.

What Burglars Actually Do

A University of North Carolina at Charlotte study surveyed 422 convicted burglars:

  • 83% said they checked for an alarm system before breaking in
  • But only 60% said alarms would deter them — they simply moved to the next house
  • The top deterrent wasn’t alarms — it was signs of occupancy (lights on, car in driveway, TV sounds)

Entry points are boringly predictable:

  • 34% enter through the front door (often unlocked)
  • 23% through a first-floor window
  • 22% through the back door

A third of burglaries happen through an unlocked door. No amount of smart sensors helps if you leave the front door open.

The critical insight the entire security industry buries: the appearance of presence is more effective than any sensor, camera, or monitoring service.

The DIY Security Stack That Actually Works

Layer 1: Physical Deterrence (~€250)

The unglamorous stuff security companies never mention because there’s no recurring revenue in it.

Reinforced door frames and strike plates — €60

Most doors are kicked in, not picked. Standard strike plates are held by two screws barely an inch long. Replace with 3mm steel reinforcement plates secured by 75mm screws reaching structural studs. This single upgrade makes forced entry dramatically louder, harder, and slower.

Security film on ground-floor windows — €120

Holds shattered glass together, turning a quick smash-and-reach into a prolonged, noisy wrestling match.

Exterior motion-activated lights — €70

Burglars hate light. A College of Policing (UK) study found improved lighting reduced crime by 21%.

Layer 2: Smart Detection (~€450)

Local-recording cameras (Reolink RLC-810A × 3) — €270

Record to a local NAS. No cloud. No subscription. No footage handed to police without consent.

Zigbee door/window sensors (Aqara × 8) — €80

Connected to Home Assistant. Instant notifications — no monitoring center middleman.

Zigbee motion sensors (Aqara FP2 × 2) — €100

mmWave presence sensors detect humans even standing still. Smart enough to differentiate between a cat and a person — eliminating the false alarm problem.

Layer 3: Smart Response (€0 — Just Automations)

This is where Home Assistant earns its reputation.

Automation 1: “The house is alive”

When the alarm triggers (any sensor while in “away” mode), simultaneously:

  • All interior lights flash on/off twice, then stay on
  • Living room TV turns on at high volume
  • Smart speaker announces “SECURITY ALERT — RECORDING IN PROGRESS”
  • All exterior lights go to maximum brightness

This creates the most effective deterrent: the appearance that someone is home and aware. Most burglars are already running.

Automation 2: “The evidence collector”

  • All cameras switch to maximum resolution continuous recording
  • 30-second video clips sent to phone via Telegram
  • Snapshots emailed as redundant backup
  • Critical phone alert with camera feeds

Actionable video in 5 seconds. Not a phone call asking for your verbal password.

Automation 3: “The neighbor alert”

If the alert isn’t dismissed within 60 seconds, Home Assistant messages two trusted neighbors to check the house.

Automation 4: “Vacation mode”

Randomized presence simulation:

  • Lights in realistic patterns through the evening
  • TV during typical viewing hours
  • Blinds with ±15 minute daily offsets
  • Porch light on sunset-triggered schedule

Dynamic simulation that changes daily — indistinguishable from actual occupancy.

The Real Cost Comparison

Professional Monitored System (10-year total)

Item Cost
Equipment + installation €500–€1,500
Monthly monitoring (36-month contract) €1,080
Years 4–10 (ongoing) €2,520
10-year total €4,100–€5,100

Plus: cloud dependency, privacy concerns, false alarm fines, deprioritized police response.

DIY Setup (10-year total)

Item Cost
Physical deterrence €250
Cameras (Reolink × 3) €270
Door/window sensors (Aqara × 8) €80
Motion sensors (Aqara FP2 × 2) €100
Home Assistant Green €100
Total €800
Monthly cost €0

“But Who Calls the Police?”

Your phone does. Immediately. With video evidence.

“I have a live camera feed showing someone breaking into my house right now” gets a very different police response than a monitoring center calling with “We have an unverified alarm activation.”

In 2026, your phone is the monitoring center. Push notifications are instantaneous. Video is streamable from anywhere.

The Bottom Line

Physical deterrence and the appearance of occupancy stop burglaries. Smart detection and instant personal alerts handle the rest. Professional monitoring is an expensive middleman adding latency to a process that needs to be instant.

Your home doesn’t need a security company. It needs reinforced doors, visible cameras, smart automations, and you — with a phone full of instant alerts and live video.

The security industry won’t tell you this, because a one-time €800 customer is worth a lot less than a €360/year subscriber locked into a three-year contract.

Originally published at SmartHomeMade. For more no-BS smart home advice: camera recommendations at Best No-Subscription Security Cameras and hub comparisons at Home Assistant vs SmartThings vs Hubitat.

Codacy vs ESLint: Quality Platform vs JS Linter

Codacy screenshot

Quick Verdict

Codacy screenshot

Codacy and ESLint are not direct competitors – they operate at fundamentally different levels of the development workflow. Comparing them is like comparing a project management platform to a to-do list app. Both help you track work, but the scope, depth, and intended audience are entirely different. ESLint is a free, open-source JavaScript and TypeScript linter that runs in your editor, catches bugs and style violations in real time, and enforces team coding conventions through the largest plugin ecosystem in the JavaScript world. Codacy is a paid, cloud-hosted code quality and security platform that analyzes repositories across 49 languages, enforces quality gates on pull requests, provides SAST, SCA, secrets detection, code coverage tracking, and AI-powered code review – and it happens to run ESLint under the hood as one of its many embedded analysis engines.

The fact that Codacy embeds ESLint is the critical detail in this comparison. When you connect a JavaScript repository to Codacy, it automatically runs ESLint rules against your code alongside its other analyzers. You get ESLint coverage without configuring ESLint separately. But Codacy adds layers that ESLint cannot provide on its own – centralized dashboards, quality gate enforcement, security scanning across multiple dimensions, dependency vulnerability detection, and AI-augmented PR feedback.

Choose ESLint alone if: you are a solo developer or small team (1-5 people) working exclusively in JavaScript/TypeScript, you want zero cost, you do not need centralized quality dashboards or security scanning, and the real-time editor feedback loop is all you need for code quality enforcement.

Choose Codacy (which includes ESLint) if: you work in a team of 5 or more developers, you need multi-language analysis across your stack, you want quality gates that block non-compliant PRs, you need security scanning (SAST, SCA, secrets detection) without adding separate tools, and you want AI-powered PR review and organizational code health dashboards.

The most common setup: Run ESLint locally in your editor for instant feedback as you type, and run Codacy in CI/CD for comprehensive PR analysis, quality gate enforcement, and security scanning. This gives you the fastest possible feedback (ESLint in the editor) and the deepest possible analysis (Codacy on every PR). The two tools complement each other rather than compete.

At-a-Glance Comparison

Category Codacy ESLint
Type Cloud code quality and security platform JavaScript/TypeScript linter
Languages 49 (JavaScript, Python, Java, C#, Go, PHP, Ruby, Kotlin, Swift, Rust, and 39 more) JavaScript, TypeScript (via plugins: JSX, Vue, Svelte)
Uses ESLint internally Yes – embeds ESLint as one of its JS/TS analysis engines N/A – is ESLint
Analysis engines Multiple embedded engines per language (ESLint, Pylint, PMD, SpotBugs, Bandit, etc.) Single engine with pluggable rules
Total rules Thousands across 49 languages via embedded engines ~300 core + thousands via community plugins
Runs in Cloud (CI/CD, PR analysis, scheduled scans) Editor (real-time), CLI, pre-commit hooks, CI
Real-time editor feedback Via AI Guardrails (VS Code, Cursor, Windsurf) Native – runs as you type with zero delay
Quality gates Yes – configurable, PR-blocking No built-in quality gates
SAST (security scanning) Yes – built-in across all supported languages Limited – basic patterns via eslint-plugin-security
SCA (dependency scanning) Yes – scans for known CVEs in dependencies No
Secrets detection Yes – detects committed credentials No
Code coverage tracking Yes – integrates with test coverage reports No
Duplication detection Yes No
AI code review AI Reviewer (hybrid rule + AI analysis) No
AI code governance AI Guardrails (free IDE extension for AI-generated code) No
Custom rules Configure embedded engine rules via dashboard Fully programmable (write custom rules in JavaScript)
Plugin ecosystem Curated embedded engines Massive open-source ecosystem (2,900+ npm packages)
Configuration Web dashboard (code patterns) Config files (eslint.config.js)
Git platforms GitHub, GitLab, Bitbucket Not tied to any platform – runs anywhere
Pricing Free (Guardrails only); Pro $15/user/month Completely free and open source
Self-hosted Business plan only (custom pricing) N/A – runs locally by design

What Is Codacy?

Codacy is a cloud-native code quality and security platform that provides automated static analysis, security scanning, coverage tracking, and AI-powered code review across 49 programming languages. Founded in 2012, Codacy serves over 15,000 organizations and 200,000 developers. The platform’s core value proposition is consolidation – instead of assembling separate tools for linting (ESLint), security scanning (Semgrep or Snyk), coverage tracking (Codecov), and code review (manual or AI-assisted), Codacy packages all of these capabilities into a single platform with a unified dashboard and predictable per-user pricing.

For JavaScript and TypeScript projects specifically, Codacy embeds ESLint as its primary analysis engine. When you connect a JavaScript repository, Codacy automatically runs ESLint rules against your code on every pull request. But ESLint is just one of many engines Codacy runs. For Python, it runs Pylint and Bandit. For Java, it runs PMD and SpotBugs. For Go, it runs Gosec. For each language, Codacy selects and embeds the most appropriate community analysis tools, wraps their output in a unified interface, and adds platform-level features on top.

The platform-level features are what differentiate Codacy from running ESLint directly. Quality gates allow teams to define conditions that code must meet before it can be merged – minimum coverage on new code, zero new security vulnerabilities, maximum complexity thresholds. When a PR fails the quality gate, Codacy posts the failure as a status check on GitHub, GitLab, or Bitbucket, and teams can configure branch protection to block the merge. SAST provides security vulnerability detection across all supported languages. SCA scans dependency manifests for known CVEs. Secrets detection catches accidentally committed API keys and credentials. AI Guardrails is a free IDE extension that scans code – including AI-generated code – in real time in VS Code, Cursor, and Windsurf. AI Reviewer combines deterministic analysis with context-aware AI to provide higher-level PR feedback on logic, complexity, and missing test coverage.

Codacy’s setup is pipeline-less by default. Connect your Git platform account, select repositories, and analysis begins on the next pull request. There is no CI/CD configuration required, no scanner to install, and no infrastructure to manage. Total time from signup to first PR analysis: under 10 minutes. For advanced features like code coverage tracking, a CI step is needed to upload coverage reports, but the core scanning experience requires zero pipeline configuration.

For a detailed breakdown of Codacy’s capabilities, see our Codacy review. For pricing details, see our Codacy pricing guide.

What Is ESLint?

ESLint is a pluggable linting tool for JavaScript and TypeScript, originally created by Nicholas Zakas in 2013. It is maintained by the OpenJS Foundation and is the de facto standard linter for the JavaScript ecosystem. ESLint analyzes source code to find problematic patterns, enforce coding conventions, and catch potential bugs before runtime.

ESLint runs locally – in the developer’s editor as a real-time extension, as a CLI command during development, in pre-commit hooks via tools like Husky and lint-staged, and optionally in CI/CD pipelines. The key distinction is that ESLint provides immediate, in-editor feedback. When a developer writes if (x = 5) instead of if (x === 5), ESLint highlights the issue instantly with a red underline and an explanation. This tight feedback loop is ESLint’s core value proposition – catching issues at the moment they are written, not minutes or hours later in a CI pipeline or PR review.

ESLint’s rule system is infinitely extensible. The core package includes approximately 300 rules covering potential errors, best practices, variable handling, and stylistic conventions. The ecosystem expands this dramatically through community plugins. Popular plugins include @typescript-eslint (TypeScript-specific rules and parser), eslint-plugin-react and eslint-plugin-react-hooks (React best practices), eslint-plugin-vue (Vue.js rules), eslint-plugin-import (module import/export conventions), eslint-plugin-jsx-a11y (accessibility rules), eslint-plugin-security (basic security patterns), and eslint-plugin-prettier (Prettier integration). Teams can also write entirely custom rules in JavaScript by traversing the AST, making ESLint adaptable to any coding standard or convention.

In 2024, ESLint completed its migration to the “flat config” system (eslint.config.js), replacing the legacy .eslintrc format. ESLint 9.x dropped support for the legacy format entirely. The flat config simplifies configuration, makes rule resolution more predictable, and is now the default for all new projects.

ESLint is completely free under the MIT license. There is no paid edition, no premium tier, and no usage limits. The total cost of ESLint is zero dollars, forever, regardless of team size, codebase size, or commercial usage. This zero-cost model is one of ESLint’s strongest advantages and a critical factor in this comparison – every dollar spent on Codacy is a dollar more than ESLint costs.

Feature-by-Feature Breakdown

Multi-Language Support vs JavaScript/TypeScript Focus

This is the most fundamental structural difference between the two tools.

ESLint analyzes JavaScript and TypeScript exclusively. Through plugins, it extends to JSX, TSX, Vue single-file components, Svelte components, and Astro files. ESLint does not analyze Python, Java, Go, Ruby, PHP, or any other language. This focus is a strength for JavaScript teams – ESLint’s JavaScript analysis is unmatched in depth and ecosystem breadth. But it means that a team building a Node.js backend with a Python ML service and Go microservices would need ESLint for the Node.js code, Ruff or Pylint for Python, and golangci-lint for Go – three separate tools with three separate configurations, three separate CI steps, and no unified dashboard.

Codacy analyzes 49 languages through a single platform. JavaScript, TypeScript, Python, Java, C#, Go, PHP, Ruby, Kotlin, Swift, Scala, Rust, Dart, Elixir, Shell, Terraform, Dockerfile, and dozens more. Each language is analyzed by one or more embedded engines appropriate for that language. The results from all engines across all languages appear in a single dashboard with unified quality metrics, trend tracking, and quality gate enforcement. A team working across a polyglot stack sees all of their code quality data in one place.

The practical impact for JavaScript-only teams: If your entire codebase is JavaScript and TypeScript, Codacy’s multi-language support is not a differentiator. You are paying for a platform that supports 49 languages when you use one or two of them. ESLint covers your language needs perfectly and for free. The case for Codacy in a JavaScript-only environment rests entirely on the platform features (quality gates, security scanning, coverage tracking, AI review) rather than language breadth.

The practical impact for polyglot teams: If your stack includes languages beyond JavaScript, Codacy’s multi-language support becomes a genuine advantage. Instead of configuring and maintaining separate linters for each language, Codacy provides consistent analysis across the entire codebase. The unified dashboard shows quality trends for JavaScript, Python, Java, and every other language side by side. Quality gates apply across the entire PR regardless of which languages the changes touch. For teams managing 5+ languages, this consolidation has real operational value.

Cloud Dashboard vs CLI Workflow

ESLint is a command-line tool with editor integration. You configure it through a config file, run it in the terminal or editor, and read the output as text. There is no web dashboard, no trend tracking, no historical analysis. ESLint tells you what is wrong with your code right now. If you want to track how your code quality changes over time, you need to build your own reporting on top of ESLint’s output or use a platform like Codacy or SonarQube that provides dashboards.

Codacy is a web platform with a centralized dashboard. When you log into Codacy, you see your organization’s repositories, each with a quality grade, issue count, coverage percentage, and security vulnerability summary. Clicking into a repository shows historical trends – how your issue density, coverage, and duplication have changed over the past weeks and months. Pull request views show the specific issues introduced in each PR, which quality gate conditions passed or failed, and inline code annotations pointing to exact problem locations.

For solo developers and small teams, ESLint’s CLI output is perfectly adequate. You know your codebase, you can grep the terminal output, and you fix issues as they appear. For teams of 10 or more, especially distributed teams where engineering leads need visibility into code health across multiple repositories, Codacy’s dashboard provides organizational awareness that ESLint cannot offer. The ability to answer “which of our 20 repositories has the worst code coverage?” or “has our security vulnerability count been trending up or down?” requires a platform, not a CLI tool.

The dashboard is also where Codacy’s quality gate configuration lives. Engineering leads can define organization-wide quality standards – minimum coverage on new code, maximum complexity per function, zero critical security issues – and apply them across all repositories. This centralized management is valuable for organizations that need consistent standards without relying on individual teams to configure their own ESLint rules correctly.

Security Scanning

This is the dimension where the gap between Codacy and ESLint is widest, and the primary technical reason to consider Codacy alongside or instead of standalone ESLint.

ESLint’s security coverage is minimal and pattern-based. Plugins like eslint-plugin-security detect approximately 15 common anti-patterns – use of eval(), innerHTML assignments, child_process.exec() with unsanitized input, SQL string concatenation, non-literal require() calls, and similar surface-level patterns. These rules catch the most obvious security mistakes within a single file. They do not perform data flow analysis. If a developer passes user input through a helper function that calls another function that eventually reaches a dangerous sink like a database query, eslint-plugin-security cannot trace that flow. This is a fundamental limitation of single-file, AST-based analysis – not a criticism of ESLint, which was never designed to be a security scanner.

Codacy provides multi-dimensional security scanning. The Pro plan ($15/user/month) includes:

  • SAST (Static Application Security Testing): Detects injection vulnerabilities, authentication issues, cryptographic weaknesses, insecure data handling, and other vulnerability patterns across all 49 supported languages. The security analysis goes beyond single-file pattern matching, though it does not match the taint analysis depth of dedicated SAST tools like Semgrep or SonarQube’s Developer Edition.
  • SCA (Software Composition Analysis): Scans dependency manifests (package.json, requirements.txt, pom.xml, and others) to identify known CVEs in open-source packages. This catches vulnerabilities in your npm dependencies that ESLint has no awareness of. When a critical CVE is disclosed in a package your application uses, Codacy alerts you.
  • Secrets Detection: Scans for accidentally committed API keys, database passwords, authentication tokens, and other credentials. ESLint does not detect committed secrets.

The Business plan adds DAST (Dynamic Application Security Testing) powered by ZAP, which tests running applications for runtime vulnerabilities that static analysis cannot detect.

The practical gap is significant. Consider a typical Express.js application. ESLint with eslint-plugin-security might catch a direct eval() call. It will not catch a SQL injection vulnerability where user input from req.body flows through a service layer to an unsanitized database query. It will not detect that your jsonwebtoken dependency has a critical CVE. It will not flag an AWS access key accidentally committed in a configuration file. Codacy catches all three categories of issues. For applications that handle user data, process payments, store credentials, or are accessible from the internet, the security gap between ESLint alone and Codacy is meaningful.

For teams that want security scanning without Codacy’s full platform, dedicated security tools like Semgrep (free open-source CLI with taint tracking) or Snyk Code (developer-first SAST with SCA) are alternatives. See our Semgrep vs ESLint comparison for a detailed breakdown of ESLint plus security scanning options.

Pull Request Integration and Quality Gates

ESLint has no built-in PR integration or quality gates. You can run ESLint in CI and fail the build if errors are found, but this is a binary pass/fail based on whether any ESLint rule violations exist. ESLint does not post inline comments on pull requests, does not provide quality metrics beyond pass/fail, and does not track trends. You can achieve basic PR integration by configuring your CI to run npx eslint . and using the exit code as a GitHub status check, but the developer experience is limited – you see “ESLint failed” with no inline annotation of where the problems are (unless you add a third-party action).

Codacy provides comprehensive PR integration and quality gate enforcement. When a pull request is opened, Codacy automatically analyzes the changes and posts results directly to the PR:

  • Inline comments appear on specific lines of code highlighting issues with severity ratings, descriptions, and suggested fixes.
  • Quality gate status appears as a status check on the PR. If the quality gate fails (new issues found, coverage dropped, security vulnerability detected), the check fails and teams can configure branch protection to block the merge.
  • AI Reviewer feedback provides context-aware comments that consider the entire PR – changed files, PR description, linked Jira tickets – and flags logic errors, missing test coverage, and overly complex functions.
  • Summary comments show a breakdown of new issues by category (bugs, code smells, security vulnerabilities, duplication) and the coverage change introduced by the PR.

Quality gates are arguably the most important platform feature that ESLint cannot replicate. A quality gate allows engineering leads to define conditions like “new code must have at least 80% coverage,” “no new security vulnerabilities,” and “code complexity must not exceed a threshold.” These conditions are enforced automatically on every PR, preventing code quality from degrading over time regardless of who is writing or reviewing the code. ESLint can enforce coding conventions but cannot enforce coverage, security, or complexity thresholds because those metrics are outside its scope.

For teams that have experienced the slow degradation of code quality despite having ESLint configured – coverage dropping, complexity increasing, security issues accumulating – quality gates are the missing enforcement mechanism. ESLint prevents individual bad patterns. Quality gates prevent systemic quality degradation.

Custom Rules and Configuration

ESLint’s custom rule system is the most flexible in the JavaScript ecosystem. Developers write rules in JavaScript by defining AST visitor functions. The learning curve is moderate – most experienced JavaScript developers can write basic custom rules within a few hours. The plugin system allows packaging rules into shareable npm packages, and the ecosystem contains over 2,900 ESLint-related packages. Shared configurations like eslint-config-airbnb, @antfu/eslint-config, and eslint-config-standard let teams adopt comprehensive rule sets with a single install.

Configuration lives in the repository as code (eslint.config.js), which means it is version-controlled, reviewed in pull requests, and deployed alongside the application. Developers have direct control over every rule – enabling, disabling, changing severity, and configuring rule-specific options. This developer-centric configuration model gives individual teams maximum flexibility to customize their linting experience.

Codacy’s rule configuration is centrally managed through the web dashboard. Codacy calls its rule configuration “code patterns.” Administrators navigate to the code patterns settings for a repository, select which embedded engine rules to enable or disable, and configure severity levels. The configuration is managed through the web UI rather than through config files in the repository. This centralized approach is valuable for organizations that want consistent standards across all repositories without relying on individual teams to maintain config files, but it removes the direct, version-controlled configuration that developers are accustomed to with ESLint.

Codacy does not support writing custom analysis rules from scratch. You can enable or disable the rules provided by the embedded engines (ESLint rules, Pylint rules, PMD rules, etc.), but you cannot add rules that do not exist in any of Codacy’s embedded engines. If your team has custom ESLint rules – perhaps enforcing internal API conventions, company-specific naming patterns, or framework-specific best practices – those custom rules will not be available in Codacy unless they are part of a publicly available ESLint plugin that Codacy includes.

The trade-off is flexibility vs simplicity. ESLint gives developers full control over every aspect of linting configuration, including the ability to write arbitrary custom rules. Codacy gives organizations centralized management of pre-existing rules across multiple languages, but restricts customization to what the embedded engines support. For teams with complex, project-specific linting requirements, ESLint’s flexibility is essential. For teams that want standard best-practice rules applied consistently across all repositories, Codacy’s centralized management reduces configuration drift.

IDE Support and Developer Experience

ESLint’s IDE integration is the gold standard for JavaScript development. The ESLint VS Code extension is installed on over 30 million VS Code instances. It runs ESLint in the background as you type, highlighting issues with red and yellow underlines, providing fix suggestions through the quick-fix menu, and optionally auto-fixing issues on save. The experience is instantaneous – there is zero delay between writing code and seeing feedback. Extensions are also available for JetBrains IDEs (WebStorm, IntelliJ), Vim/Neovim, Sublime Text, and virtually every other editor. For most JavaScript developers, ESLint’s editor integration is the primary way they interact with the tool.

Codacy’s IDE presence is through AI Guardrails. Guardrails is a free extension for VS Code, Cursor, and Windsurf that scans code in real time – including AI-generated code from tools like GitHub Copilot. Using MCP (Model Context Protocol) integration, Guardrails detects security and quality issues and can auto-remediate them before code is even printed to the editor. The auto-fix capability is especially valuable in AI-assisted development workflows where code is generated rather than typed.

The critical distinction is scope. ESLint in the editor catches everything its configured rules cover – code style, potential bugs, import conventions, React hook violations, accessibility issues, and dozens of other categories through its plugin ecosystem. Guardrails focuses specifically on security and quality issues, particularly in AI-generated code. Guardrails is not a general-purpose linter – it does not flag unused variables, inconsistent naming, or incorrect import ordering the way ESLint does.

The recommended setup is both. Run ESLint in your editor for comprehensive real-time linting. Run Codacy Guardrails for security-focused scanning of AI-generated code. Run Codacy in CI/CD for PR analysis and quality gate enforcement. Each tool provides feedback at a different scope and stage, and none of them conflicts with the others.

Pricing Comparison

ESLint Pricing

ESLint is completely free. There is no paid tier, no enterprise edition, no per-user or per-repo licensing. The core package, all official plugins, the VS Code extension, and the flat config system are free and open source under the MIT license. The total cost of ESLint is zero dollars, forever.

This zero-cost model means every dollar spent on Codacy represents additional cost on top of what ESLint provides for free. The question is whether Codacy’s platform features justify that additional cost.

Codacy Pricing

Plan Price What You Get
Developer (Free) $0 AI Guardrails IDE extension for VS Code, Cursor, Windsurf
Pro $15/user/month Unlimited scans, repos, LOC. AI Guardrails + AI Reviewer. SAST, SCA, secrets detection. Coverage tracking, duplication detection, quality gates. GitHub, GitLab, Bitbucket integration
Business Custom Everything in Pro + DAST (ZAP-powered), AI Risk Hub, self-hosted option, SSO/SAML, audit logs, dedicated support

For a detailed pricing breakdown with cost scenarios, see our Codacy pricing guide.

Side-by-Side Cost at Scale

Team Size ESLint Cost (Annual) Codacy Pro Cost (Annual) What Codacy Adds Over ESLint
1 dev (solo) $0 $180 Quality gates, SAST, SCA, secrets, coverage, AI review
5 devs (startup) $0 $900 Same as above, plus centralized dashboard for team visibility
10 devs $0 $1,800 Same, plus organizational consistency across repositories
20 devs $0 $3,600 Same, plus enforcement that scales beyond what code review alone can achieve
50 devs $0 $9,000 Same, plus the alternative is assembling 3-4 separate tools (linter + SAST + SCA + coverage)
100 devs $0 $18,000 Same, plus centralized governance that manual enforcement cannot achieve at this scale

Key Pricing Observations

ESLint’s cost advantage is absolute for pure linting. If your only requirement is JavaScript/TypeScript code quality enforcement through configurable rules with real-time editor feedback, ESLint provides this at zero cost. No alternative – not Codacy, not SonarQube, not any other platform – beats free.

Codacy’s value proposition is consolidation, not replacement. The $15/user/month is not paying for “better ESLint.” It is paying for quality gates, SAST, SCA, secrets detection, coverage tracking, AI-powered review, and centralized dashboards – features that ESLint does not provide at any price. The relevant comparison is $15/user/month for Codacy vs the cost of assembling equivalent capabilities from separate tools (SonarQube or Semgrep for SAST, Snyk for SCA, Codecov for coverage, CodeRabbit for AI review). By that measure, Codacy’s pricing is competitive.

For teams under 5 developers, ESLint alone is often sufficient. The operational overhead of adding a paid platform, configuring quality gates, and managing dashboard permissions may not be justified when the same 3-5 people are writing, reviewing, and deploying all the code. Code review catches what ESLint misses. At 5+ developers, the need for automated enforcement becomes real – code review quality varies, standards drift across repositories, and security issues slip through manual review.

The free tier gap matters. ESLint’s free tier is the full product – every feature, every plugin, unlimited use. Codacy’s free tier is limited to the AI Guardrails IDE extension. There is no free tier for centralized repository analysis, PR scanning, or quality gates. Teams evaluating Codacy cannot try the full platform without a paid subscription or trial. By contrast, SonarQube offers a free self-hosted Community Build and a free Cloud tier for up to 50K lines of code, making it easier to evaluate before committing budget. See our Codacy vs SonarQube comparison for details.

Use Cases – When to Choose Each

When ESLint Alone Is Enough

Solo developers and very small teams (1-3 people). When the entire team can communicate across a desk (or a short Slack thread), centralized quality dashboards and automated quality gates are solving a problem that does not exist yet. Everyone knows the codebase, everyone reviews each other’s code, and ESLint in the editor catches the mechanical issues. Adding Codacy at this stage is premature optimization of process.

Open-source libraries and packages. Most open-source JavaScript packages use ESLint (and often Prettier) as their entire quality toolchain. The scope is narrow (one package, one language), the contributors are experienced, and the code undergoes thorough public review. Quality gates and SAST scanning add less value when the code is reviewed by dozens of open-source contributors. The community’s GitHub Actions workflows typically include ESLint as a CI check, which is sufficient.

Simple JavaScript/TypeScript projects without sensitive data. If you are building a marketing site, a component library, an internal dashboard that sits behind a VPN, or a prototype that will never handle user credentials or financial data – ESLint with a solid configuration catches the code quality issues that matter, and the security scanning gap is not a meaningful risk.

Budget-constrained teams. ESLint is free. If $15/user/month for Codacy is outside your budget, ESLint with a good configuration file, Prettier for formatting, and Husky for pre-commit hooks provides a solid quality baseline at zero cost. You can add free security scanning through the Semgrep open-source CLI or SonarQube Community Build if needed.

When Codacy Makes Sense

Growing teams (5+ developers) where code quality consistency matters. As teams grow beyond the point where everyone reviews everyone else’s code, quality standards start to drift. Different developers configure ESLint differently (or forget to configure it at all). PRs get approved with insufficient test coverage because reviewers are busy. Security issues slip through because no one checks for them systematically. Codacy’s quality gates create an automated enforcement layer that maintains standards regardless of who is writing or reviewing the code.

Multi-language projects. If your stack includes Python, Java, Go, Ruby, or other languages alongside JavaScript, Codacy provides consistent analysis across the entire codebase through a single platform. The alternative is configuring and maintaining separate linters for each language with no unified dashboard, no cross-language quality gates, and no centralized trend tracking. For polyglot teams, Codacy’s 49-language support is a genuine operational advantage over ESLint’s JavaScript-only scope.

Applications that need security scanning. If your application handles user authentication, processes payments, stores personal data, or is accessible from the internet, you need security scanning beyond what eslint-plugin-security provides. Codacy’s SAST, SCA, and secrets detection catch vulnerability categories that ESLint fundamentally cannot detect. The alternative is assembling separate tools – Semgrep for SAST, Snyk for SCA, a secrets scanner – which provides deeper coverage in each dimension but adds operational complexity and cost.

Teams heavily using AI coding assistants. If your developers generate substantial code through GitHub Copilot, Cursor, or Claude, Codacy’s AI Guardrails scans AI-generated code in real time in the IDE. The AI Reviewer provides context-aware PR feedback that evaluates AI-generated code for logic errors, security issues, and missing test coverage. No ESLint configuration catches the category of issues that arise specifically from AI-generated code – overly confident but subtly incorrect implementations, missing edge case handling, and security anti-patterns that AI models learn from training data.

Organizations that need quality governance and reporting. Engineering managers who need to answer questions like “what is our average code coverage across all repositories?” or “how many security vulnerabilities did we introduce this quarter?” or “which teams are consistently failing quality gates?” need a platform with dashboards and historical trend data. ESLint produces point-in-time output with no organizational aggregation or historical tracking.

Can You Use Both Together?

Yes, and this is the recommended approach for most teams that adopt Codacy. The two tools are not redundant because they operate at different stages of the development workflow and provide different types of feedback.

The Complementary Workflow

  1. Developer writes code. ESLint runs in the editor (VS Code extension) and highlights issues in real time – unused variables, missing return types, incorrect hook usage, import ordering violations, and hundreds of other rules from configured plugins. Developer fixes issues as they type. Codacy Guardrails runs simultaneously in the same editor, scanning for security issues and AI-generated code problems.

  2. Developer commits code. Pre-commit hook runs ESLint (via lint-staged and Husky) and blocks the commit if there are errors. This catches anything the developer missed or ignored in the editor.

  3. Developer opens a pull request. Codacy automatically analyzes the PR – running ESLint alongside its other analysis engines, checking security rules, scanning dependencies for CVEs, detecting secrets, and evaluating coverage impact.

  4. Codacy posts results to the PR. Inline comments appear on specific lines with issue descriptions. The AI Reviewer adds context-aware feedback. The quality gate status check indicates pass or fail. If the gate fails, the PR is blocked from merging.

  5. Developer addresses feedback, pushes again. Codacy re-analyzes. Gate passes. PR is merged.

Why Not Just Use Codacy Alone?

The main reason to keep ESLint in the editor is speed of feedback. ESLint highlights issues as you type – instantaneously. Codacy analysis runs when a PR is opened or updated, which means there is a delay between writing code and seeing Codacy’s feedback. If you rely solely on Codacy, you might write an entire feature with style violations, unused variables, and convention deviations, push it as a PR, wait for Codacy analysis, then fix dozens of issues that ESLint would have caught in real time. The round-trip time is frustrating and wasteful.

Additionally, ESLint’s plugin ecosystem provides framework-specific linting that Codacy’s embedded ESLint may not fully replicate. If your project uses eslint-plugin-react-hooks to enforce the Rules of Hooks, eslint-plugin-jsx-a11y for accessibility checks, or a company-specific plugin with internal coding standards, your local ESLint configuration likely includes plugins and rules that Codacy’s embedded ESLint does not run. Running ESLint locally ensures your team’s full rule set is applied.

Configuration Alignment

One practical consideration when running both tools is rule alignment. Your local ESLint configuration (eslint.config.js) and Codacy’s code patterns settings are independent. If you enable a rule locally that is disabled in Codacy (or vice versa), developers will see inconsistent feedback – ESLint flags an issue in the editor that Codacy does not flag in the PR, or Codacy flags something in the PR that the developer never saw locally. This is not a catastrophic problem, but it can be confusing.

To minimize inconsistency, audit the ESLint rules enabled in Codacy’s code patterns settings and align them with your local configuration. Focus on the rules that matter most – potential bugs, security anti-patterns, and complexity thresholds – and accept that perfect alignment across every stylistic rule is not worth the maintenance effort.

Alternatives to Consider

If neither Codacy’s platform approach nor ESLint’s standalone linting fits your needs perfectly, several alternatives are worth evaluating.

SonarQube is the most direct alternative to Codacy as a code quality platform. SonarQube provides 6,500+ deterministic rules across 35+ languages, the most mature quality gate enforcement in the market, technical debt tracking with remediation time estimates, and self-hosted deployment options. The Community Build is free for self-hosted deployment (20+ languages, basic quality gates). SonarQube Cloud Free supports up to 50K lines of code with branch and PR analysis at no cost. For JavaScript, SonarQube runs its own proprietary analyzer (not ESLint), and SonarLint provides synchronized IDE feedback. SonarQube’s analysis depth per language exceeds Codacy’s because it uses purpose-built rules rather than embedded third-party engines. However, SonarQube lacks Codacy’s SCA (except in Enterprise), has no DAST, and its AI features are less mature. See our SonarQube vs ESLint comparison and Codacy vs SonarQube comparison for detailed breakdowns.

Semgrep is the leading alternative for security scanning specifically. If your primary gap is security – not code quality dashboards or coverage tracking – Semgrep’s open-source CLI provides SAST with taint tracking and cross-file analysis at no cost. The Semgrep AppSec Platform (free for up to 10 contributors) adds SCA with reachability analysis, secrets detection with validation, and AI-powered triage. Semgrep is purpose-built for security and provides deeper vulnerability detection than Codacy’s SAST. However, Semgrep does not provide code quality metrics, quality gates, coverage tracking, or duplication detection. The typical pairing is ESLint for code quality plus Semgrep for security. See our Semgrep vs ESLint comparison for details.

DeepSource is a modern code quality platform with 5,000+ analysis rules and an emphasis on low false positive rates. DeepSource’s AI-powered Autofix automatically generates fixes for detected issues. At $12/user/month for the Team plan, it is priced below Codacy. DeepSource supports JavaScript, TypeScript, Python, Go, Java, Ruby, and other languages. Its five-dimension PR report cards (bugs, anti-patterns, performance, security, style) provide more granular quality assessment than Codacy’s unified quality grade. However, DeepSource supports fewer languages than Codacy (16 vs 49) and does not offer SCA or DAST.

Snyk Code is the best option if security scanning across code, dependencies, containers, and infrastructure is your primary requirement. Snyk’s DeepCode AI engine provides SAST with interfile data flow analysis, and Snyk Open Source provides SCA with reachability analysis – determining whether vulnerable code paths in your dependencies are actually called by your application. For JavaScript teams that need the deepest possible security coverage, Snyk Code plus ESLint is a strong combination. See our Codacy vs Snyk comparison for details.

Biome is worth considering as an ESLint replacement (not a Codacy replacement) for teams that want a faster, all-in-one linter and formatter. Built in Rust, Biome runs 10-100x faster than ESLint and combines linting with formatting (replacing both ESLint and Prettier). Its rule set is growing but does not yet match ESLint’s plugin ecosystem breadth. Biome is not a platform and does not provide quality gates, security scanning, or dashboards.

For a broader view of code quality tools, see our best code quality tools roundup and our Codacy alternatives guide.

Final Recommendation

The Codacy vs ESLint comparison is not a choice between two competing tools – it is a decision about how much tooling your team needs beyond the linting baseline.

ESLint is the foundation. Every JavaScript and TypeScript project should use ESLint. It is free, universal, instantaneously integrated into every editor, and catches the vast majority of code quality issues at the earliest possible point – while you are writing the code. ESLint plus Prettier plus a good shared configuration gives you a solid baseline that costs nothing and takes minutes to set up. This is not debatable. ESLint is the minimum viable code quality setup for JavaScript development.

Codacy is the platform layer on top of the foundation. For teams that need more than linting – quality gate enforcement, security scanning (SAST, SCA, secrets), code coverage tracking, AI-powered PR review, and organizational quality dashboards – Codacy packages these capabilities into a single cloud platform at $15/user/month. The fact that Codacy embeds ESLint internally means you get ESLint coverage as part of the package, but Codacy adds dimensions of analysis that ESLint cannot provide at any price.

For solo developers and very small teams (1-3): ESLint + Prettier. Add Codacy’s free AI Guardrails IDE extension for security scanning of AI-generated code. This setup costs $0 and provides excellent code quality coverage.

For small teams (5-10 developers) in JavaScript/TypeScript: ESLint + Prettier in the editor. Evaluate Codacy Pro ($15/user/month) when you need quality gates, security scanning, and centralized dashboards. The alternative is assembling free tools – ESLint + SonarQube Community Build + Semgrep OSS – which provides similar coverage at zero cost but with more operational overhead.

For growing teams (10-50 developers) across multiple languages: ESLint + Prettier for JavaScript/TypeScript. Codacy Pro for platform-level quality and security analysis across all languages. The $15/user/month cost is justified by the consolidation of quality gates, SAST, SCA, secrets detection, and coverage tracking into a single tool. The alternative – SonarQube for quality gates plus Snyk Code for security plus Codecov for coverage – provides deeper analysis in each dimension but costs more and requires managing multiple vendor relationships.

For enterprise organizations (50+ developers): ESLint + Prettier for JavaScript/TypeScript. Either Codacy Business or SonarQube Enterprise for platform-level governance, depending on whether you prioritize breadth and simplicity (Codacy) or depth and self-hosted deployment (SonarQube). Add CodeRabbit for AI-powered PR review if Codacy’s AI Reviewer does not meet your requirements. Add Semgrep for deeper SAST if Codacy’s built-in security scanning is insufficient for your compliance needs.

The bottom line is straightforward. ESLint is free and essential – use it always. Codacy is a paid platform that adds security, governance, and AI capabilities on top – use it when your team outgrows what a standalone linter can enforce. The two tools work together rather than against each other, and most teams that adopt Codacy continue running ESLint in the editor for the fast feedback loop that no platform can replace.

Further Reading

Frequently Asked Questions

Does Codacy use ESLint?

Yes. Codacy embeds ESLint as one of its analysis engines for JavaScript and TypeScript code. When you connect a JavaScript repository to Codacy, it runs ESLint rules alongside other analyzers automatically. This means you get ESLint coverage without configuring ESLint separately. Codacy wraps ESLint’s output into its unified dashboard, adding severity ratings, trend tracking, and quality gate enforcement on top of ESLint’s raw findings. You can also configure which ESLint rules are active through Codacy’s code patterns settings.

Do I need both Codacy and ESLint?

It depends on your workflow. If you use Codacy, you technically get ESLint analysis included because Codacy runs ESLint under the hood for JavaScript and TypeScript. However, Codacy runs in CI/CD and on pull requests – it does not provide real-time editor feedback as you type. Most developers still benefit from running ESLint locally in their editor (via the VS Code extension) for instant feedback, while Codacy handles centralized analysis, quality gates, and security scanning at the PR level. The combination gives you fast editor feedback (ESLint) plus deep platform analysis (Codacy).

Is ESLint free?

Yes, ESLint is completely free and open source under the MIT license. There is no paid edition, no premium tier, and no usage limits. The entire ecosystem – ESLint core, the flat config system, community plugins like eslint-plugin-react and @typescript-eslint, and shared configurations like eslint-config-airbnb – is free to use in any project, commercial or otherwise. ESLint is maintained by the OpenJS Foundation and funded through sponsorships and donations. This is one of ESLint’s strongest advantages – there is zero cost regardless of team size, codebase size, or usage volume.

How much does Codacy cost?

Codacy offers a free tier that includes the AI Guardrails IDE extension for VS Code, Cursor, and Windsurf. The Pro plan costs $15/user/month and includes unlimited scans, unlimited lines of code, unlimited repositories, AI Guardrails, AI Reviewer, SAST, SCA, secrets detection, code coverage tracking, duplication detection, and quality gates. The Business plan has custom pricing and adds DAST scanning, self-hosted deployment, SSO/SAML, audit logs, and the AI Risk Hub. All paid plans use per-user pricing with no line-of-code caps.

Can Codacy replace ESLint entirely?

For CI/CD and PR-level analysis, yes – Codacy can replace a standalone ESLint CI step because it runs ESLint internally and applies the results to pull requests. However, Codacy cannot replace ESLint’s real-time editor integration. ESLint’s VS Code extension runs as you type, highlighting issues instantly with red underlines and providing auto-fix suggestions on save. Codacy’s IDE presence is through AI Guardrails, which focuses on security and AI-generated code issues rather than general linting. For the fastest possible feedback loop during development, you still want ESLint in your editor even when using Codacy in CI.

What languages does Codacy support besides JavaScript?

Codacy supports 49 programming languages through its embedded analysis engines. Beyond JavaScript and TypeScript, this includes Python, Java, C#, Go, PHP, Ruby, Kotlin, Swift, Scala, Rust, Dart, Elixir, Shell, and many others. Codacy also analyzes infrastructure-as-code formats like Terraform, Dockerfile, and CloudFormation. Each language is analyzed by one or more embedded engines – for example, Python is analyzed by Pylint and Bandit, Java by PMD and SpotBugs, and JavaScript by ESLint. This multi-language coverage is one of Codacy’s primary advantages over ESLint, which only supports JavaScript and TypeScript.

Is Codacy better than ESLint for security scanning?

Yes, significantly. ESLint’s security coverage is limited to plugins like eslint-plugin-security, which catch surface-level patterns (eval usage, innerHTML assignments) within single files. Codacy includes full SAST (Static Application Security Testing), SCA (Software Composition Analysis for dependency vulnerabilities), and secrets detection in its Pro plan. Codacy can detect injection vulnerabilities, authentication issues, cryptographic weaknesses, insecure data handling, and known CVEs in your npm dependencies. For teams that need security scanning beyond basic pattern matching, Codacy provides a substantial upgrade over ESLint alone.

Can I use Codacy’s free tier for JavaScript projects?

Codacy’s free tier is limited to the AI Guardrails IDE extension for VS Code, Cursor, and Windsurf. This extension scans code in real time in the editor for security and quality issues, including AI-generated code. However, the free tier does not include centralized repository analysis, pull request scanning, quality gates, dashboards, or any of the platform features that differentiate Codacy from ESLint. For those capabilities, you need the Pro plan at $15/user/month. If you want free centralized analysis, ESLint in CI (completely free) or SonarQube Community Build (free, self-hosted) are better options.

What is the best alternative to both Codacy and ESLint?

The closest alternative that combines linting and platform-level analysis is SonarQube. SonarQube provides 6,500+ deterministic rules across 35+ languages, quality gate enforcement, coverage tracking, and security scanning. The Community Build is free for self-hosted deployment, and SonarQube Cloud Free supports up to 50K lines of code. For JavaScript specifically, SonarQube has its own JS/TS analyzer with 500+ rules, and SonarLint provides synchronized IDE feedback similar to ESLint. DeepSource is another alternative with 5,000+ rules and AI-powered auto-fix at $12/user/month. For pure JavaScript linting, Biome is emerging as a faster alternative to ESLint.

Does Codacy work with GitHub, GitLab, and Bitbucket?

Yes. Codacy integrates with GitHub, GitLab, and Bitbucket. You connect your Git platform account, select repositories, and Codacy begins analyzing pull requests automatically. The integration includes PR decoration (inline comments on code issues), quality gate status checks, and repository-level dashboards. Codacy does not support Azure DevOps. ESLint, by contrast, is not tied to any Git platform – it runs locally or in any CI/CD system. If you use Azure DevOps, Codacy is not an option and you should consider SonarQube or running ESLint directly in your Azure Pipelines.

How does Codacy’s AI Reviewer compare to ESLint?

They are fundamentally different. ESLint is a deterministic rule engine – it applies configured rules to your code and reports violations. Every run produces the same results for the same code. Codacy’s AI Reviewer is a hybrid system that combines deterministic rule-based analysis with context-aware AI reasoning. The AI component considers the full pull request context – changed files, PR description, linked Jira tickets – and provides feedback on logic errors, missing test coverage, overly complex functions, and discrepancies between the PR description and actual code changes. ESLint cannot do any of this. The AI Reviewer is an additional layer on top of the deterministic analysis, not a replacement for it.

Should solo developers use Codacy or just ESLint?

For most solo developers, ESLint with a good shared configuration (like @antfu/eslint-config or eslint-config-airbnb) plus Prettier for formatting is sufficient. The operational overhead and cost of Codacy ($15/month) may not be justified when you are the only person writing and reviewing code. ESLint catches code quality issues for free. If you need security scanning, the free Codacy AI Guardrails IDE extension or the free Semgrep Community Edition provides SAST at no cost. Codacy’s platform features – quality gates, team dashboards, PR analysis – deliver the most value for teams of 5 or more where consistent code quality across multiple contributors matters.

Originally published at aicodereview.cc