I turned every n8n node into a machine-readable dataset (524 nodes, free) so agents can build workflows

I have been writing agents that build n8n workflows. The hard part is not “call the n8n API and post a workflow JSON.” The hard part is “pick the right node, with the right operation, with the right parameters, without hallucinating fields that do not exist.”

The n8n GUI is the source of truth. The TypeScript source files are the second source of truth. Neither is a thing you can hand to an LLM at inference time.

So I extracted everything into one structured catalog and put it on HuggingFace.

524 nodes. Every operation. Every credential type. Properties schema. Free. CC-BY-4.0.

Why a catalog and not “just scrape n8n.io”

Three real problems with leaving this implicit:

  1. Hallucination cost is high. An LLM that invents a slack.sendDM operation will produce a workflow that imports fine and fails at runtime. Hard to detect, expensive to debug.
  2. Context window pressure. Dropping the entire n8n source tree into a prompt is not realistic. You want a compact index the agent can search.
  3. Coverage is non-obvious. There are two source packages (nodes-base and @n8n/nodes-langchain), and the split between them is not visible in the UI.

The catalog flattens all of that into one row per node.

What is in each row

Field What it is
node_name Internal id (e.g. slack, airtable, lmChatOpenAi)
display_name UI label
categories Top-level categories (Communication, AI, Data and Storage)
subcategories Leaf taxonomy values
group input, output, or transform
version Default version for multi-version nodes
description One-liner
credentials_required Credential type names (e.g. slackApi, openAiApi)
operations_supported Operation values for the node
properties_schema JSON describing top-level property descriptors
source_package nodes-base or @n8n
source_file_path Repo-relative path to the .node.ts
github_permalink Pinned GitHub link to the source

Format: JSON and Parquet (Snappy). License: CC-BY-4.0. Updates monthly.

A sample row

Here is the Slack node, trimmed:

{
  "node_name": "slack",
  "display_name": "Slack",
  "categories": ["Communication"],
  "group": ["transform"],
  "version": "2.3",
  "description": "Send and read messages, manage channels",
  "credentials_required": ["slackApi"],
  "operations_supported": ["message", "channel", "user", "reaction"],
  "properties_schema": "[{"name":"resource","type":"options"},{"name":"operation","type":"options"}]",
  "source_package": "nodes-base",
  "github_permalink": "https://github.com/n8n-io/n8n/blob/stable/packages/nodes-base/nodes/Slack/Slack.node.ts"
}

And an AI node, to show the cross-package coverage:

{
  "node_name": "lmChatOpenAi",
  "display_name": "OpenAI Chat Model",
  "categories": ["AI"],
  "subcategories": ["Language Models", "Chat Models (Recommended)"],
  "group": ["transform"],
  "version": "1.3",
  "credentials_required": ["openAiApi"],
  "source_package": "@n8n",
  "source_file_path": "packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts"
}

Numbers I did not expect

A few things that fell out of the catalog once it existed:

  • 431 nodes from nodes-base, 93 from @n8n/nodes-langchain. The langchain side is a real and growing chunk.
  • The single most common credential type, by a wide margin, is httpBasicAuth (because the generic HTTP Request node is everywhere). After that the long tail starts immediately.
  • A non-trivial number of nodes have an empty operations_supported list. Those are usually root nodes (LLMs, vector stores, output parsers) where the “operation” abstraction does not apply.

Useful to know if you are writing a planner that filters by operation.

How agents actually use it

from datasets import load_dataset

ds = load_dataset("automatelab/n8n-nodes-catalog")["train"]

# Filter to nodes that can post messages somewhere
messaging = ds.filter(
    lambda r: "message" in (r["operations_supported"] or [])
)
for row in messaging:
    print(row["node_name"], row["credentials_required"])

Typical pipeline:

  1. Embed every row (description, operations, credentials) into a vector store.
  2. At plan time, retrieve the top N nodes for a user request.
  3. Hand the agent only those rows. Compact context, no hallucinated operations.
  4. The agent emits an n8n workflow JSON. Validation against properties_schema catches malformed configs before deploy.

This is the same shape as RAG over a tool catalog, which is becoming a pattern in its own right.

Caveats

  • The properties schema is a top-level summary, not the full recursive parameter tree. For deep parameter shapes the github_permalink is your friend.
  • Multi-version nodes only report the default version. If you need every version of a node, the source link covers it.
  • License is CC-BY-4.0 on the catalog additions; the n8n source itself is governed by n8n’s own license, which you should respect when you ship.

Links

If you build agent tooling on top of this, the thing I would most like to see is an open eval set: prompts in, expected n8n workflow JSON out. That is the next obvious missing piece, and I do not think anyone has shipped one yet.

Strangler Fig Pattern for .NET Modernization: How It Works in a Real Production System

The strangler fig pattern is the most practical approach to modernizing a legacy .NET monolith without stopping product delivery. It works by incrementally replacing functionality in the existing system with new services, routing traffic gradually from the old codebase to the new one until the legacy system can be decommissioned. The pattern does not require a feature freeze, does not demand a big-bang cutover, and does not force you to bet the entire modernization project on a single deployment. At Blackthorn Vision, a Microsoft Solutions Partner specializing in .NET modernization and Azure architecture, we use this approach as the default for enterprise .NET platforms where product delivery cannot pause for a rewrite.

This article covers how the pattern actually works in a .NET production context, what the implementation looks like with modern tooling, where it tends to break down, and how to sequence the migration to avoid the failure modes that affect a large share of strangler fig projects.

Why teams reach for the strangler fig pattern

The alternative to the strangler fig pattern is usually described as a big-bang rewrite: stop adding features to the legacy system, build a new version from scratch, and cut over when it is ready. The appeal is obvious. You start with a clean architecture, no legacy constraints, and the full benefit of everything the team has learned since the original system was built.

The problem is that big-bang rewrites fail at a rate that should make any engineering leader uncomfortable. Modernization Intel’s analysis of enterprise strangler migration data from 2022 to 2025 found a 76% success rate across 29 tracked strangler fig projects, with median annual savings of $640K in successful engagements. Failed projects, by contrast, produced a median sunk cost of $2.1 million. A key finding from the same dataset: projects that extracted less than 5% of monolith functionality in the first 90 days had a 92% failure rate, which means early velocity is the strongest predictor of whether a strangler migration succeeds. The most common failure mode in rewrites is feature parity: the new system consistently runs behind the legacy system in capability, the cutover date slips repeatedly, and eventually leadership loses confidence and either cancels the project or forces a cutover before the new system is ready.

The strangler fig pattern sidesteps this problem by keeping the legacy system in production and making the migration reversible at every step. If a newly migrated component behaves incorrectly in production, you route traffic back to the legacy implementation while you investigate. There is no moment where the entire system depends on code that has never handled real production load.

The technical implementation for .NET: YARP as the facade layer

The strangler fig pattern requires a routing layer that sits in front of both the legacy system and the new services. In the .NET ecosystem, the recommended tool for this is YARP (Yet Another Reverse Proxy), a Microsoft-developed reverse proxy library built on ASP.NET Core middleware. Microsoft’s own migration guidance for incremental ASP.NET to ASP.NET Core migrations is built around YARP, and it has become the standard approach for .NET strangler fig implementations because it integrates naturally with the existing .NET toolchain.

The setup works like this. You create a new ASP.NET Core project that hosts YARP. Initially, YARP forwards 100% of requests to the legacy .NET Framework application. As you migrate each component, you add routing rules to YARP that send specific routes or request types to the new service instead of the legacy system. The legacy application continues to run and handle everything that has not yet been migrated. From the perspective of users and external systems, nothing changes, because all requests still arrive at the same endpoint.

                ┌─────────────────────────────────────┐
                │            YARP Facade               │
 User / Client ►│         (ASP.NET Core app)           │
                │                                      │
                │  Route: /api/reports ───────────────► New .NET 8 Service
                │  Route: /api/orders  ───────────────► New .NET 8 Service
                │  Route: everything else ────────────► Legacy .NET Framework App
                └─────────────────────────────────────┘

Both systems run in production simultaneously. The routing configuration in YARP is the only thing that changes as each component is migrated. Rolling back a component means updating one routing rule, not redeploying the entire application.

The practical implementation steps for a .NET Framework to .NET 8 migration are:

  • Deploy the YARP-based ASP.NET Core application to Azure App Service alongside the legacy .NET Framework application. Both services run independently, with YARP configured to proxy all traffic to the legacy system as a starting point.

  • Add the System.Web.Adapters library to both projects, which provides compatibility shims for HttpContext and related types, allowing code that references System.Web to be moved incrementally without rewriting everything that depends on it.

  • Identify the first component to migrate, ideally something with clear boundaries, reasonable test coverage, and meaningful traffic volume. Starting with a low-traffic component that nobody will notice if it breaks is tempting, but it delays the point at which the team learns how the migration behaves under real load.

  • Build the new implementation in the ASP.NET Core project, run it in parallel with the legacy implementation, compare outputs to confirm parity, and then update the YARP routing configuration to direct that component’s traffic to the new service.

  • Monitor the component in production for a validation period before moving on to the next component. The length of this period depends on the criticality of the component and the traffic patterns it handles.

How to pick the first component to migrate

Choosing the wrong starting point is one of the most common reasons strangler fig projects stall in the first two months. The instinct is usually to start with something small and contained, which makes sense in principle but often produces a migration that validates the toolchain without validating the approach under realistic conditions.

The criteria that produce a better first component are:

  • Clear external boundaries: the component has a defined API surface that other parts of the system consume through a stable contract, rather than reaching into shared state or calling internal methods directly.

  • Measurable output: you can run both implementations against the same inputs and compare outputs programmatically, which is the foundation of the parallel-run validation that makes the strangler fig safe.

  • Meaningful traffic: the component handles enough requests that production behavior is visible in monitoring within hours, not weeks. This matters because some failure modes only appear under load or in edge cases that staging environments do not produce reliably.

  • Limited data coupling: the component does not share database tables with multiple other components in ways that make schema changes a cross-system coordination problem.

At Blackthorn Vision, the components we typically migrate first in a .NET Framework monolith are API endpoints that handle well-defined request and response contracts, reporting and data export functions that can be validated by comparing output files, and background processing jobs that can be run in parallel and compared before the legacy version is disabled.

The parallel-run validation approach

Running both implementations in parallel and comparing their outputs is the technical mechanism that makes the strangler fig pattern safe. Without it, you are deploying new code to production and hoping it behaves correctly, which is not meaningfully different from a big-bang migration in terms of risk.

The parallel-run works by having the YARP facade send each request to both the legacy implementation and the new service simultaneously, recording both responses, and logging any discrepancies. The legacy response is returned to the caller, so users always receive the behavior they expect. The new service response is compared in the background. Discrepancies trigger alerts that the team investigates before increasing the traffic percentage routed to the new service.

This approach requires investment in observability infrastructure that many legacy .NET systems lack. If the existing system has no structured logging, no distributed tracing, and no way to correlate requests across services, that investment has to happen before the migration can proceed safely. The observability work is not overhead: it is the foundation that makes the parallel-run comparison meaningful and that gives the team confidence to increase the traffic percentage routed to the new implementation.

What breaks in practice, and why

Research covering 29 tracked strangler fig projects found that projects missing more than two key prerequisites had a 94% failure rate. The prerequisites that matter most in a .NET context are test coverage on the components being migrated, a working parallel-run validation mechanism, and a data migration strategy for components that own data.

The failure mode we see most often at Blackthorn Vision is what might be called “facade as decoration”: a team builds the YARP routing layer, migrates the UI or the API layer of one component, but leaves the business logic and data access in the monolith. The new service makes calls back into the legacy system for data, which means the coupling has not been reduced, it has just been made visible through a network boundary. The team has added latency and operational complexity without actually strangling anything.

The solution to this is enforcing data sovereignty as a hard rule: each migrated service must own its data. If a new service needs to read data that currently lives in the monolith’s database, the migration plan for that service must include a data migration strategy, either through dual-write during the transition, Change Data Capture (CDC) to synchronize data between the old and new stores, or a data extraction and import step that runs before traffic is routed to the new service.

The database trap deserves its own mention because it is the failure mode most likely to cause data corruption rather than just downtime. When two systems, the legacy monolith and the new service, write to the same database table simultaneously without a coordination mechanism, race conditions and conflicting writes produce corrupted records that are often invisible until a business process produces wrong results days later. This is not a theoretical risk: it is what happens when teams treat the shared database as a neutral middle ground between the old and new systems instead of recognizing it as the source of coupling they are trying to remove.

The correct approach is to never allow both systems to write to the same table at the same time. If data has to be shared during the transition, use either dual-write with application-level coordination (the new service writes to both the new store and the legacy table, and the legacy system reads from its own table) or Change Data Capture to synchronize records between the old and new data stores without allowing both systems to write the same rows. Neither approach is simple, but both are substantially safer than allowing concurrent writes to shared tables.

Other common failure points are:

  • Session state: .NET Framework applications often use in-process session state, which breaks immediately when traffic starts flowing through a YARP proxy to a different process. Externalizing session state to a shared Redis cache or Azure Cache for Redis before the migration begins removes this as a blocker.

  • Authentication: shared authentication tokens and cookies that were issued by the legacy system need to be validated by the new service. This typically requires externalizing the identity provider and configuring both systems to validate tokens from the same source.

  • Synchronous integrations that cannot tolerate the additional latency introduced by the proxy hop. Most integrations handle this without issue, but any integration with a timeout configured below 500ms should be identified during the assessment phase and addressed before the facade is deployed.

Sequencing the full migration

A strangler fig migration for a mid-size .NET Framework monolith typically runs over 12 to 18 months when executed alongside normal product delivery. That timeline is longer than most teams expect when they start, and shorter than most teams fear when they look at the size of the codebase.

The migration progresses in three broad phases. The first phase establishes the infrastructure: YARP is deployed, observability is in place, the parallel-run validation mechanism is working, and the first component has been migrated and validated under real production load. This phase typically takes six to eight weeks and is the most important: if the infrastructure is not solid, every subsequent migration step will be slower and riskier than it needs to be.

The second phase is the main migration loop: one component per sprint, parallel-run validation, traffic ramp, monitoring period, then the next component. The speed of this phase depends on the quality of the boundaries in the original system. Components with clear boundaries migrate quickly. Components where business logic is scattered across stored procedures, event handlers, and configuration files take longer because the boundary has to be established before the migration can happen.

The third phase is decommissioning: once all traffic has been routed to the new services, the legacy system enters a monitoring-only state for a final validation period, typically four to six weeks, before it is shut down. The YARP facade can be removed at this point or retained as a load balancer, depending on the architecture of the new system.

The Azure development services that support this migration are available and well-documented. Azure App Service hosts both systems during the parallel-run phase. Azure Cache for Redis externalizes session state. Application Insights provides the observability layer. One additional benefit that teams often underestimate until they see it in practice: moving from .NET Framework to .NET 8 removes the dependency on Windows Server, which means the new services can run in Linux containers on Azure Kubernetes Service or Linux-based App Service plans. For organizations running large fleets of Windows Server VMs, the licensing cost reduction from this shift alone can be substantial, and it becomes a secondary justification for the modernization investment that is easy to quantify for leadership.

Why this matters for AI integration

One of the main reasons enterprise teams modernize .NET Framework monoliths today is that modern AI tooling works substantially better on modern .NET architecture. Azure OpenAI integration, Semantic Kernel, and the Microsoft.Extensions.AI libraries that simplify LLM orchestration all depend on async patterns, clean service boundaries, and the observability infrastructure that legacy monoliths typically lack.

At Blackthorn Vision, the strangler fig approach is often used as a prerequisite step before Azure OpenAI or Semantic Kernel integration, because the AI workload exposes exactly the coupling and latency problems that the monolith has been hiding. Most of the platforms we modernize have teams that want to add copilot features, semantic search, or LLM-powered internal tools to an existing product. The strangler fig migration creates the service boundaries and the async infrastructure that make those integrations sustainable in production, rather than brittle demos that fail under real load.

This is why enterprise teams searching for companies with real AI integration experience in .NET often end up evaluating partners who can do both: assess and modernize the platform, and then build the AI layer on top of the architecture that modernization produced.

Who this engagement model fits

Blackthorn Vision is often brought in when enterprise teams need to modernize a legacy .NET monolith without pausing feature delivery, particularly when the codebase has accumulated enough complexity that a full rewrite carries unacceptable risk. Most of the platforms we work on have been in production for 8 to 15 years, support thousands of daily users, and involve complex ERP integrations or multi-team delivery environments where downtime is not acceptable. The strangler fig pattern with YARP is the approach we use for .NET Framework 4.x to .NET 8 migrations, and the case studies on the Blackthorn Vision site reflect the range of platforms and industries where we have applied it.

This makes Blackthorn Vision relevant for CTOs and engineering leaders searching for the best companies for legacy .NET modernization, particularly when the requirement is a partner who can own the architectural decisions and manage the migration risk, not a team that needs to be told what to do at each step.

If you are evaluating whether the strangler fig pattern is the right approach for your platform, the most useful first step is usually an honest assessment of the two things that determine whether the pattern will work: whether the existing system has enough boundary definition to support incremental extraction, and whether the team has the observability infrastructure to validate parity during the parallel-run phase. Blackthorn Vision’s application modernization and assessment approach starts with exactly those two questions, and verified client feedback on how the engagements play out is available on the Blackthorn Vision Clutch profile.

Boston Dynamics, Atlas’ı mini buzdolabı taşırken gösteren bir video paylaştı

Boston Dynamics, Atlas

Boston Dynamics, Atlas'ı mini buzdolabı taşırken gösteren bir video paylaştıHyundai çatısı altında yer alan ABD merkezli Boston Dynamics, resmi X hesabından insansı robotu Atlas’ı ufak boyutlu buzdolabını taşırken gösteren bir video paylaştı. Önümüzdeki yıllarda Hyundai’nin otomobil fabrikalarında da görev yapacak Atlas, tamamen elektrikli altyapının temel alındığı yeni sürümüyle birçok rakibinden daha rahat hareket ediyor ve dengesini hemen her koşulda kolaylıkla koruyabiliyor. “Herkes Atlas’ın kendilerine bir içecek getirip getiremeyeceğini soruyor ama o size buzdolabının tamamını getirebiliyor. Yapay zeka destekli pekiştirmeli eğitim sürecinden güç alan Atlas, ağır işleri yaparken tüm gövdesini kullanabiliyor.” diyen şirket, “Spot” adında dört bacaklı bir robot köpek modeline de sahip. Everyone asks if Atlas can bring them a drink, but this robot can bring you the whole fridge. Using AI-driven behaviors, Atlas is doing hard work and coordinating its whole body to manage heavy objects, balancing complex contact points with accuracy and reliability. pic.twitter.com/yXG0dY6Qqf — Boston Dynamics (@BostonDynamics) May 18, 2026

Boston Dynamics, Atlas’ı mini buzdolabı taşırken gösteren bir video paylaştı yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

I’m done. Flow editors are broken. And you all know it.

I spent the last years trying to make flow editors work. For chatbots. For forms. For LLM orchestration. Every single time I end up in the same place: buried in nested menus, drowning in node configurations, writing inline code in a tool that promised me “no-code.”

I’m done making excuses for these tools. They are broken. Not the UI. Not the docs. The foundation.

So you open Botpress. Or Voiceflow. Or n8n. And within five minutes you’re staring at a canvas full of nodes, nested menus, inline code editors, and condition builders that feel like they need their own documentation. You wanted to build a chatbot. Now you’re studying a tool.

And it’s not just chatbots. Complex form flows, onboarding wizards, LLM orchestration. Every single flow builder out there follows the same pattern: drag a node, click on it, get buried in configuration. Add a condition? Another menu. Need an API call? Another node, another config panel. Want to change something? Good luck finding where that logic actually lives.

This is not normal. And we should stop pretending it is.

The code editor test

Let me ask you something. Have you ever clicked on a variable name in your code editor and suddenly 10 nested submenus popped open? With configuration options, conditions, and inline logic attached to that single variable?

No? Of course not. That would be insane. No developer would accept that. You’d close the editor and never look back.

So why do we accept exactly this in flow builders? Every node is a little universe of hidden complexity. Click on it, and you’re three menus deep before you even understand what it does. That’s not a powerful tool. That’s a broken abstraction.

If a flow editor requires a course before you can use it, it’s not a tool. It’s a problem.

The real issue is deeper than UI

The frustrating thing is: most people blame the UI. “It just needs a better interface.” “A cleaner design.” “Fewer options.”

No. The UI is a symptom. The actual problem is the underlying architecture.

Think about it. An edge between two nodes is already a condition. It says: if this, then that. That’s temporal logic. That’s a rule. It’s right there, visible on the canvas.

So why does every flow builder then bury another layer of conditions inside the nodes? You connect two nodes with an edge, and then you click on the node and configure more conditions in a nested menu. Conditions hidden inside a box. Invisible from the outside. Black boxes everywhere.

And it gets worse. Most flow builders love super-nodes. Take a typical “Choice Node” or “Question Node.” It contains the question, the answer options, the branching logic, sometimes even validation. All packed into one node. That’s a conceptual breach. The answer options are not part of the graph. They are buried inside a single node, invisible to the structure, unreachable by edge logic.

What if each answer option was its own node? Connected by edges that already carry the logic? Then your entire decision tree would be visible on the canvas. No hidden menus. No black boxes. The graph is the logic. This applies to every complex node type out there. The moment a node contains structure that should live in the graph, you’ve lost the point of having a graph in the first place.

And there is another problem: Most flow builders are event-driven at their core. Something happens, a trigger fires, a sequence runs top to bottom, done. That model has no real concept of state. It doesn’t know where you are. It doesn’t adapt when things change. So every edge case, every branch, every “what if the user goes back” scenario has to be manually wired and configured inside the nodes. The architecture can’t handle the logic on a structural level, so it dumps it on you, hidden behind click after click after click.

What if the graph could think for itself?

Imagine a different foundation. Instead of “event happens, sequence runs,” the graph itself holds the state. Every node knows if it’s alive or dead. Every connection between nodes has a clear semantic meaning: this path must be taken, this path may be taken, this path must not be taken.

Three edge types. Must. May. Not. That’s it. And that’s enough. These three types can express every logical gate visually on the canvas. AND, OR, NOT, XOR. No configuration panels. No inline code. Just edges you can see.

When state changes, the graph re-sequences itself. You don’t manually wire fallback paths. You don’t write inline conditions. The structure is the logic. Change a decision and entire branches die while others come alive. No custom code. No nested menus. The graph adapts because it was designed to.

And before someone asks: no, re-sequencing doesn’t mean the entire graph is traversed every time. At the start, only the entry node is active. As the user progresses, the active portion grows, but it stabilizes at a fraction of the total graph. When a user backtracks, the old branch dies and a new one activates. The traversal shifts, it doesn’t accumulate. The more complex your graph, the more branches are dead at any given moment. Complexity makes it more efficient, not less.

This concept is called Reactive Graph Sequencing. And I built a flow editor based on it.

Wanderer

Wanderer is an open source flow editor built on RGS. No nested menus. No inline code. No configuration hell. The graph structure itself defines the logic.

If you want to understand the technical foundation in detail, I wrote about it here: Reactive Graph Sequencing: The Technology Behind Wanderer

Want to see demos? Check this out

I’m not saying this is the only way. But I am saying that the current generation of flow builders is built on a broken foundation. And we should stop blaming ourselves for struggling with tools that were never designed to be intuitive.

What do you think? Is the architecture the real problem, or am I just bad at reading nested menus?

Title image: https://pixabay.com/de/illustrations/ai-generiert-scrapyard-wagen-8241457/

5 Best Python Vector Database Libraries

Figure 1: Client architecture

Most comparisons of Python vector database libraries focus on retrieval speed, indexing algorithms, or benchmark results. These metrics matter, but production failures stem from various factors: installation inconsistencies, client packaging differences, version churn, and unexpected API changes. In reality, a different class of problems appears once the application leaves the notebook environment and runs inside a production service.

A typical example occurs with embedded ChromaDB setups. A project may work perfectly during development, only to fail in production with an error such as:

RuntimeError: Chroma running in http-only client mode.

A structural conflict between the chromadb and chromadb-client packages triggers this error because the client-only package lacks the default embedding functions the application depends on. Diagnosing this can take hours.

Client packaging choices and library design decisions, not retrieval quality or indexing performance, produce this type of failure.

This article compares the leading Python vector database libraries from that perspective, examining client architecture, installation stability, API design, and long-term maintainability, rather than benchmark numbers alone.

TL;DR

  • ChromaDB: Fastest setup for prototyping and notebook environments with minimal configuration.
  • Pinecone: Fully managed cloud solution with zero infrastructure management overhead.
  • Qdrant: Zero code changes from local development to production; the strongest open-source option for API stability.
  • Weaviate: Hybrid search combining vector similarity and keyword filtering at scale.
  • Actian VectorAI DB: On-premises deployment with same architecture from laptop to production; Actian designed it for edge and air-gapped environments.

The Python Landscape: Understanding the Options

The relationship between a Python vector database library and its storage backend determines how you will develop, test, and eventually scale your application. The wrong library choice often triggers the environment-specific failures described above, since each architecture handles local and production environments differently.

These differences generally fall into four distinct categories, each with its own approach to the interaction between infrastructure and code.

Four client architectures

  • Cloud-only (e.g., Pinecone): These clients act as a full API abstraction for serverless environments. The primary advantage is zero infrastructure management, but this requires an active internet connection and an API key for all local development and testing.

  • OSS with managed option (e.g., Qdrant, Weaviate, Milvus): This set of tools uses the same API for both self-hosted Docker instances and managed cloud services. This provides excellent development-production parity, though it often requires managing a local server or a Docker container during development.

  • Embedded libraries (e.g., ChromaDB, FAISS): These tools run in-process and embed the database logic in your Python application. While they are ideal for notebooks and rapid prototyping, their developers never designed them for distributed production environments, and they do not offer a well-defined migration path as the application scales.

  • Extension approach (e.g., pgvector via Timescale vector): This model adds vector search capabilities to traditional relational databases. It allows existing PostgreSQL infrastructure to support vector similarity search. However, query performance varies with index configuration, dataset size, and workload characteristics; some scenarios benefit from the relational foundation, while others favor purpose-built vector architectures.

Figure 1: Client architecture

These four models describe how a client connects to storage, but they also show a practical separation between standalone search libraries and managed database systems. Choosing the wrong model generates some of the most persistent production friction in vector search applications.

A vector database provides the infrastructure required for production readiness, going beyond what developers’ standalone libraries offer. Libraries such as FAISS or Annoy are static, in-memory tools focused on approximate nearest neighbor search across large datasets. They are highly efficient for similarity search within a fixed vector space, but they cannot manage data over time.

Specialized databases like Pinecone, Qdrant, or Milvus go further, providing full CRUD support, metadata-based filtering, and distributed persistence for large datasets.

The table below summarizes where each architecture fits across common use cases.

Category Primary trade-off Production migration path
Cloud-only No infrastructure management; requires network connectivity and API authentication for all environments Same client code across development and production
OSS + managed Identical API for local and cloud deployments; requires Docker or server setup for local development Zero code changes between local Docker instance and managed cloud service
Embedded In-process execution with minimal setup; limited to single-machine architecture Client class replacement required; distributed deployment needs architecture redesign
Extension Integrates with existing PostgreSQL infrastructure; performance depends on index configuration and dataset characteristics Current PostgreSQL setup and scale requirements determine the migration path

Client Comparison: Developer Experience Deep Dive

Architecture does narrow your options, but the day-to-day experience of working with a Python vector database library comes down to how each client handles connection setup, version stability, and the friction points encountered during active development.

We’re comparing the four clients below based on what developers deal with in real-world use.

1. Pinecone Python client

Pinecone offers one of the more polished connection experiences among cloud-only vector database clients, with extensive type hints and a straightforward initialization pattern.

from pinecone import Pinecone

    pc = Pinecone(api_key="your-api-key")
    index = pc.Index("your-index-name")

Strengths:

  • Extensive type hints and IDE autocompletion support.
  • Pinecone introduced AsyncIO support in v6 via Pinecone Asyncio.
  • gRPC mode offers higher throughput for demanding workloads.
  • Well-maintained official documentation.

Pain points:

  • Pinecone shipped three major versions in 18 months (v5, v6, and v7), introducing breaking changes to the connection logic and renaming the package from pinecone-client to pinecone.
  • Historical confusion between the pinecone and pinecone-client packages.
  • query_namespaces async operations under load require thread pool tuning.

When to choose Pinecone:

  • Fully managed infrastructure is a hard requirement.
  • The team has no appetite for self-hosted database management.
  • The Budget allows for cloud-only pricing at the target scale.

When to avoid Pinecone:

  • Local development requires a live API key.
  • API stability across versions is a priority.
  • Cost at scale above 100M vectors is a constraint.

2. Weaviate Python client

Weaviate’s v4 client is a meaningful step forward from v3, adding typed classes and gRPC support that noticeably improve query performance.

import weaviate

    client = weaviate.connect_to_local()
    collection = client.collections.get("your-collection-name")

Strengths:

  • gRPC mode delivers 40-70% faster query performance than v3.
  • Typed property and DataType classes replace untyped v3 dictionaries.
  • Built-in hybrid search combining vector and keyword search.
  • Strong support for multi-tenancy workloads.

Pain points:

  • Weaviate has fully deprecated the v3 API, and teams report that the migration takes weeks of work.
  • gRPC requires port 50051 to be open, which creates friction in restricted network environments.
  • Batch API redesign caused significant confusion (Issue #433).
  • LangChain did not ship v4 support until several months after Weaviate’s release (Issue #14531).

When to choose Weaviate:

  • Hybrid search combining vector similarity and keyword filtering is a core requirement.
  • gRPC performance gains justify the network configuration overhead.
  • The team has the capacity to manage the v3-to-v4 migration.

When to avoid Weaviate:

  • Migration resources are limited, and API stability is a priority.
  • Network environments restrict non-standard port access.

3. ChromaDB Python client

ChromaDB offers one of the easiest onboarding experiences among Python vector database libraries, making it a natural starting point for notebooks and early-stage prototyping.

 import chromadb

    # In-memory mode
    client = chromadb.Client()

    # Persistent mode
    client = chromadb.PersistentClient(path="/your/path")

    # HTTP client mode
    client = chromadb.HttpClient(host="localhost", port=8000)

Strengths:

  • Simplest API surface of any client in this comparison.
  • Mature LangChain integration with well-documented examples.
  • In-memory mode requires zero configuration for notebook environments.
  • Large and active open-source community.

Pain points:

When to choose ChromaDB:

  • Rapid prototyping in a notebook environment is the primary use case.
  • The dataset fits comfortably within a single process.
  • LangChain integration with minimal configuration is a priority.

When to avoid ChromaDB:

  • Production deployment on Windows or Python 3.13 is required.
  • The application needs to scale beyond a single machine.
  • A clear and well-defined migration path to a distributed infrastructure is important.

4. Qdrant Python client

Developers value Qdrant for its local-to-production parity. The same client code runs against an in-memory instance during development and a fully managed cloud deployment in production, without requiring any modifications.

from qdrant_client import QdrantClient

    # In-memory mode for local development
    client = QdrantClient(":memory:")

    # Production mode - zero code changes required
    client = QdrantClient(url="https://your-cluster-url", api_key="your-api-key")

Strengths:

  • :memory: mode enables a zero-code-change local-to-production workflow.
  • Qdrant introduced a native AsyncQdrantClient for high-concurrency workloads.
  • Pydantic model type safety throughout the client interface.
  • Rust-backed implementation with lower memory overhead compared to JVM-based alternatives.

Pain points:

  • Developers must explicitly set prefer_grpc=True to enable gRPC, a step they often overlook.
  • Port split between REST (6333) and gRPC (6334) requires careful network configuration.
  • Pydantic version constraints: v1.10.x or v2.21 and above only.
  • Cloud connection issues (Issue #112).

When to choose Qdrant:

  • Local-to-production parity is a priority, and zero code changes between environments matter.
  • High-concurrency async workloads require native AsyncQdrantClient support.
  • You prefer a self-hosted, open-source vector database deployment over a managed cloud service.
  • Hybrid search that combines dense and sparse vectors is a core requirement.

When to avoid Qdrant:

  • The team has no Docker experience and needs a simpler local setup.
  • The target environment cannot support gRPC network configuration.
  • Pydantic version constraints conflict with existing project dependencies.

Each client brings distinct trade-offs to the connection layer. These differences also extend further into how each client handles installation and platform compatibility.

Installation and Environment Management

In ideal environments, installing a Python vector database library is straightforward. In practice, the target platform, Python version, and existing package dependencies each introduce variables that can turn a simple pip install into a multi-hour debugging session. A quick compatibility check before committing to a client is worth the effort, since most of these issues only surface after the setup is complete.

The compatibility matrix

The following matrix shows client behavior across Python 3.8–3.13 on macOS ARM, Windows, and Linux.

Client macOS ARM (M1/M2) Windows Linux (Debian) Python 3.13
Pinecone ✓ Full support ✓ Full support ✓ Full support ✓ Supported
Weaviate ✓ Full support ✓ Full support Requires Docker for gRPC ✓ Supported
ChromaDB hnswlib compilation errors Instability above 99 records (#3058) Requires Debian Bookworm+ ✗ Broken (#3651)
Qdrant ✓ Full support ✓ Full support ✓ Full support ✓ Supported

ChromaDB carries the heaviest compatibility burden of any client in this comparison. On macOS with ARM processors, hnswlib produces compilation errors during installation, forcing developers to manually pin Python to 3.11 or 3.12.

On Windows, ChromaDB destabilizes once a collection exceeds 99 records, making the embedded client unsuitable for anything beyond early prototyping. On Linux, Debian-based distributions require Bookworm or later to install and run ChromaDB cleanly.

Virtual environment best practices

Setting up a virtual environment before installing any vector database client saves a lot of debugging time, especially with ChromaDB, where developers know package conflicts occur frequently.

python -m venv venv
    source venv/bin/activate  # On Windows: venvScriptsactivate
    pip install chromadb 

Pinning the client version in a requirements.txt file matters equally, since several of these clients have a history of introducing breaking changes between minor releases.

chromadb==0.4.x
    qdrant-client==1.7.x
    pinecone==3.x
    weaviate-client==4.x

ChromaDB’s two-package architecture confuses many developers. When someone installs chromadb-client instead of chromadb, the application throws this error on its first attempt to call the default embedding function.

ValueError: You must provide an embedding function

chromadb-client is a lightweight HTTP-only package that does not include DefaultEmbeddingFunction. Applications that rely on the default embedding behavior need the full chromadb package instead.

Extras and optional dependencies

Beyond the base installation, each client supports additional dependencies that can improve performance for production workloads.

 # Pinecone with gRPC support
    pip install pinecone[grpc]

    # Qdrant with FastEmbed for local embedding generation
    pip install qdrant-client[fastembed]

    # ChromaDB with sentence-transformers for local embedding support
    pip install chromadb sentence-transformers

gRPC has the highest impact on optional installation for query performance. Weaviate sees 40-70% faster queries over gRPC than over REST, while Qdrant gains roughly 15% in query speed. The trade-off is that gRPC requires additional network configuration, which may not be feasible in restricted environments.

FastEmbed and sentence-transformers both enable local embedding generation without an external API dependency, keeping latency and embedding costs down for semantic search and similarity search workloads.

Qdrant’s native AsyncQdrantClient and Pinecone’s PineconeAsyncio deliver 3–5x throughput improvements under high-concurrency workloads.

Local Development Workflows

Developers make most vector database decisions in the local development environment. The critical question is: Which client requires the least code change when moving to production?

The migration path
Here is how each client handles the move from local deployment to production.

 # Qdrant - zero code changes required
    client = QdrantClient(":memory:")              # Development
    client = QdrantClient(                         # Production
        url="https://your-cluster-url",
        api_key="your-api-key"
    )

    # ChromaDB - client class change required
    client = chromadb.Client()                     # Development
    client = chromadb.HttpClient(                  # Production
        host="your-host",
        port=8000
    )

    # Pinecone - same code in both environments
    pc = Pinecone(api_key="your-api-key")          # Development and production
    index = pc.Index("your-index-name")

Qdrant :memory: mode carries identical client code from local development all the way to production. The vector store configuration, cosine similarity settings, and hnsw index parameters remain the same across environments.

ChromaDB requires a change to the client class when moving to production. The more widely the codebase uses the client, the more of the application this change touches.

Pinecone uses the same code in both development and production since everything runs in the cloud regardless of the stage.

These migration differences stem from three distinct local development approaches: embedded mode, Docker, and cloud-only.

Embedded mode

ChromaDB’s default embedded client stores data only in memory. When the application stops running, the data is lost. For developments involving persistent collections, PersistentClient writes data to disk instead.

 # In-memory only: data lost when process ends
    client = chromadb.Client()
    collection = client.create_collection("my_collection")
    collection.add(documents=["doc1", "doc2"], ids=["1", "2"])

    # Persistent local storage
    client = chromadb.PersistentClient(path="/local/path")

Qdrant’s :memory: mode uses the same client interface as a production deployment. Whatever code works locally also works in production without any changes.

client = QdrantClient(":memory:")
    client.create_collection(
        collection_name="my_collection",
        vectors_config=VectorParams(size=384, distance=Distance.COSINE)
    )

Both clients work well for early prototyping and notebook environments, and the differences surface only at the production boundary.

Docker for local development

Docker runs the vector database in an isolated local container using the same configuration as in a production deployment. Qdrant and Weaviate are both open-source vector databases that support this approach.

# Qdrant
    docker run -p 6333:6333 qdrant/qdrant
    # Weaviate
    docker run -p 8080:8080 -p 50051:50051 cr.weaviate.io/semitechnologies/weaviate:latest

Once the container is running, the client connects to localhost the same way it would to a self-hosted vector database in production.

 # Qdrant
    client = QdrantClient(url="http://localhost:6333")

    # Weaviate
    client = weaviate.connect_to_local()

The main advantage is that vector index configuration behaves the same way locally and in production, and issues that surface locally are genuine issues rather than environment-specific artifacts.

The trade-off is the overhead of Docker installation and port configuration, particularly Weaviate’s requirement for both ports 8080 and 50051.

Cloud-only development

Pinecone operates entirely in the cloud. Every operation, from index creation to vector upsert to real-time search, requires an active API key and a live network connection.

The setup overhead is minimal since there is no local infrastructure to configure or maintain. The same code runs across all environments, with API key management and network connectivity as the only constant requirements.

Figure 2: Local development workflow

Moving beyond local development, how each client integrates with the broader Python ecosystem, particularly LangChain and LlamaIndex, adds another layer to the comparison.

Integration Ecosystem: LangChain and LlamaIndex

LangChain and LlamaIndex sit at the center of most Python-based retrieval-augmented generation workflows. All four clients integrate with both frameworks, though the quality of these integrations varies.

LangChain maturity

 from langchain_pinecone import PineconeVectorStore      # Dedicated package
    from langchain_chroma import Chroma                      # Mature, widely used
    from langchain_qdrant import QdrantVectorStore           # Actively maintained
    from langchain_weaviate import WeaviateVectorStore       # Lagged during v3 to v4

Pinecone’s LangChain integration benefits from thorough documentation and a stable release history across major version changes. Teams widely rely on ChromaDB’s integration to prototype retrieval-augmented generation pipelines.

Qdrant’s dedicated langchain-qdrant package keeps pace closely with client releases. Weaviate is the exception. LangChain took several months to catch up with the v3-to-v4 migration, and the subsequent breaking changes forced many teams to pin their dependency versions until the LangChain integration caught up.

LlamaIndex support

All four clients have LlamaIndex connectors through the llama-index-vector-stores namespace.

 from llama_index.vector_stores.qdrant import QdrantVectorStore

Here, the support pattern is consistent across all four clients. When a client releases a new version, LlamaIndex integration typically follows within four to eight weeks. Any breaking changes in the client affect pipelines built on top of it in the meantime.

The integration tax

Every time a vector database client releases a major update, a predictable sequence follows.

  1. The client releases a new version with breaking changes.
  2. LangChain and LlamaIndex updated their integrations weeks later.
  3. Production pipelines built on top of those integrations break in the meantime.

This pattern has played out with Weaviate’s v3-to-v4 migration, Pinecone’s three major releases in 18 months, and ChromaDB’s ongoing compatibility issues.

API stability matters more than feature richness when selecting a Python vector database library for production retrieval-augmented pipelines. A client with fewer features but a stable API causes considerably less disruption than one with a rich feature set and frequent breaking changes.

Performance Considerations: Beyond Raw Speed

Most client comparisons overlook three factors that significantly affect vector database performance: protocol choice, the quality of async support, and connection pooling.

Protocol choice: REST vs. gRPC

gRPC and REST are the two transport protocols available across these clients. As mentioned earlier, Weaviate sees 40 to 80% faster queries over gRPC, and Qdrant gains roughly 15% in query speed with gRPC enabled. In restricted network environments where port 50051 is not accessible, REST is the more practical option.

Async support quality

Most teams build production LLM applications on FastAPI or similar async frameworks, which makes async client support a meaningful performance consideration. Using a synchronous client within an async application results in blocking calls, which sharply reduces throughput.

Qdrant’s native AsyncQdrantClient, available since v1.61, provides a well-established async implementation. Pinecone introduced PineconeAsyncio in v6, bringing proper async support to cloud-only vector search workloads. Weaviate added async support in v4.7, making it the most recent of the four to reach production-ready async capabilities. ChromaDB’s async support remains limited across all four.

The throughput difference is substantial. For I/O-bound workloads where network latency is the bottleneck, async clients typically deliver 3–5x higher throughput than their synchronous equivalents.

Connection pooling and resource management

This is one of the configuration areas where default settings tend to fall short in production. Qdrant and Pinecone both expose parameters that give more control over connection management under sustained production traffic.

 # Qdrant connection pool configuration
    client = QdrantClient(
        url="https://your-cluster-url",
        api_key="your-api-key",
        timeout=30,
        pool_size=10
    )

    # Pinecone connection pool configuration
    index = pc.Index(
        "your-index-name",
        pool_threads=30,
        connection_pool_maxsize=30
    )

For Pinecone, query_namespaces requires tuning pool_threads and connection_pool_maxsize for production workloads. For Qdrant, increasing pool_size above the default reduces connection contention for applications that handle large volumes of document embeddings in parallel.

Teams that tune these settings before deployment avoid considerable debugging time when the application runs under load.

Error Handling and Debugging

Vector database libraries handle a lot of complexity internally. When something fails, how clearly the client communicates that failure determines how quickly teams can fix it.

Error message quality

The quality of error messages varies considerably across the four clients.

Pinecone produces clear, actionable error messages that typically suggest a solution alongside the failure description, reducing the time teams spend searching for the root cause.

Qdrant error messages are helpful and point directly to the source of the problem. The UnexpectedResponse exception includes a specific reason field that identifies exactly which parameter failed validation.

    qdrant_client.http.exceptions.UnexpectedResponse: Status 400, reason: "Wrong input: Vector dimension error: expected dim: 384, got 768"

ChromaDB error messages are frequently vague and require a GitHub search to diagnose. When the two-package mix-up occurs, ChromaDB raises a ValueError about missing embedding functions instead of reporting the actual root cause. The SQLite version requirement produces a similarly unhelpful error:

 RuntimeError: Your system has an unsupported version of sqlite3. Chroma requires sqlite3 >= 3.35.0.

This error is a common roadblock for Python developers deploying on older Amazon Linux 2 or Streamlit environments.

Weaviate v3 silently failed, returning null objects or dictionaries with an errors key that developers had to check manually. The v4 rewrite addressed this with typed exceptions, such as WeaviateQueryError and WeaviateGRPCUnavailableError.

Logging and observability

Observability capabilities differ across the four clients.

  • Qdrant supports structured logging, distributed tracing, and metrics without additional configuration, which makes it a strong fit for production machine learning applications that require visibility into vector search engine performance.

  • Pinecone provides basic logging through its managed infrastructure.

  • ChromaDB has limited logging with no structured output, which makes diagnosing issues in production AI applications considerably harder.

Common pitfalls and solutions

Three error patterns recur across all four clients in production environments.

  • Client version mismatches cause frequent, unexpected failures, particularly amid Pinecone’s three releases over 18 months and Weaviate’s v3-to-v4 migration. Teams can control this by pinning client versions in a requirements.txt file.

  • Embedding dimension mismatch occurs when the query embedding dimensions do not match the collection’s expectations. Verifying that the embedding model’s output size matches the collection configuration before deployment prevents this.

  • Rate limiting affects cloud-only deployments on Pinecone and Weaviate Cloud. Implementing exponential backoff on API calls is the standard solution for production workloads that approach rate limits under sustained traffic.

How often version mismatches surface, how broadly platform incompatibilities spread, and how clearly error messages communicate failures together determine a client’s real maintenance cost in production.

The version churn across Pinecone, Weaviate, and ChromaDB has left many production teams looking for a client that prioritizes operational stability over feature velocity. Actian VectorAI DB addresses this directly.

Actian VectorAI DB

Actian designed Actian VectorAI DB for large and small-scale deployment of vector search in edge and on-premises environments, addressing operational stability through the following characteristics:

Architecture and design

  • Same architecture and APIs across all environments.
  • Docker-based deployment from local laptops to production infrastructure.
  • HNSW-based indexing for approximate nearest neighbor search.
  • Real-time indexing architecture with immediate update availability.
  • Python and JavaScript SDKs with REST and SQL APIs.
  • Native LangChain and LlamaIndex integration support.

Deployment options

  • Docker containers for local development and production.
  • Data center, private cloud, and public cloud infrastructure support.
  • Edge, remote, and air-gapped environment capability with offline operation.

Compliance design

  • On-premises deployment architecture for data sovereignty.
  • Supports GDPR, HIPAA, and data residency requirements.
  • Aligns with SOC 2 and ISO 27001 compliance frameworks.

Performance targets

  • Sub-15ms query latency goal for local deployment.
  • Uses HNSW to optimize for high recall accuracy.

These characteristics represent important considerations beyond features and benchmarks alone.

Decision Framework: Choosing Your Python Client

Selecting the right Python vector database library comes down to six criteria.

  1. Deployment model: Cloud-only deployments point towards Pinecone. On-premises or self-hosted vector database requirements point towards Qdrant, Weaviate, or Actian. Teams already running PostgreSQL should test pgvector against their workload before adding a new dependency.

  2. Team experience: Junior teams or teams new to vector databases benefit from ChromaDB or Actian, where API stability and clear error messages reduce the learning curve. Senior teams comfortable with Docker and gRPC configuration get more out of Qdrant and Weaviate.

  3. Scale:

  • Under 10 million vectors: ChromaDB or FAISS
  • 10 million to 100 million vectors: Qdrant, Pinecone, or Actian
  • Over 100 million vectors: Milvus
  1. API stability: Production environments with zero tolerance for breaking changes point towards Actian or Pinecone v7+. Teams that can absorb migrations can work with Weaviate or Qdrant. POC and experimental workloads can use ChromaDB or FAISS.

  2. Integration: LangChain-dependent pipelines should verify version support before committing to a client. Weaviate’s v3-to-v4 lag is the clearest example of what happens when this check is skipped.

  3. Cost: Pinecone’s managed pricing runs from approximately $50 to $500 or more per month, depending on scale. Cost-conscious teams running large datasets on self-hosted infrastructure should evaluate Qdrant or pgvector. ChromaDB and FAISS are both free for local, open-source vector database workloads.

Decision matrix

Criteria Pinecone Qdrant Weaviate ChromaDB Actian VectorAI DB
API stability Medium Good Improving Low High
Local development ✗ No local mode ✓ :memory: mode Docker required ✓ Embedded ✓ :memory: + SQLite
Platform compatibility ✓ Cloud only ✓ All platforms ✓ All platforms ✗ Issues on ARM, Win ✓ All platforms
Async support ✓ v6+ ✓ Native ✓ v4.7+ ✗ Limited ✓ Native
Cost $50-500+/mo Free / Self-hosted Free / Managed Free Enterprise pricing

Final Thoughts

The ChromaDB production failure from the opening example stems from client packaging issues that developers only encounter after deployment. This comparison helps avoid similar failures: platform incompatibilities, breaking changes from version migrations, and client class redesigns that propagate across codebases.

ChromaDB gets projects started quickly but tends to show its limitations once the application moves to production. Pinecone is polished and well-managed, but version churn and permanent cloud dependencies are real costs. Qdrant is the strongest open-source option for teams that want local-to-production parity without code changes. Weaviate’s v4 client significantly improves on v3 and is well-suited for teams that need hybrid search at scale.

For teams where API stability and platform compatibility are critical, enterprise-grade clients like Actian VectorAI DB provide production-ready stability with verified cross-platform support.

Explore Actian VectorAI DB for guaranteed production stability.

Vercel April 2026 Incident: Non-Sensitive Environment Variables Need Investigation Too

Vercel’s April 2026 breach started with Context.ai, a third-party AI tool.

An attacker compromised the tool’s Google Workspace OAuth app, hijacked a Vercel employee’s account, then accessed environment variables that weren’t marked “sensitive.” Vercel is now asking customers to rotate those secrets, even though they were classified as non-sensitive.

Vercel contacted the limited subset of customers whose credentials were confirmed compromised. But the broader lesson applies to any organization: a third-party OAuth compromise can cascade into internal systems fast. Vercel called the attacker “highly sophisticated” based on their speed and detailed knowledge of Vercel’s systems.

Investigate, Then Rotate

Assume any related secret is at risk until you’ve investigated it. Identify every exposed credential, check where it’s used, and confirm whether it’s active or already abused. Then revoke or rotate it, redeploy, and verify dependent services.

Vercel’s guidance is direct: use the “sensitive” flag for environment variables that contain API keys, tokens, database credentials, or signing keys. Sensitive variables are stored in a way that prevents them from being read.

Scan Your Environment Variables

If you’re a Vercel customer responding to this incident, start by pulling your environment variables locally and scanning them for exposed secrets.

Pull your environment variables for each project:

vercel env pull

Then scan the file with GitGuardian:

ggshield secret scan path .env.local

See: How To Use ggshield To Avoid Hardcoded Secrets

GitGuardian will identify which variables contain valid secrets — API keys, tokens, database credentials, signing keys. This gives you a prioritized list of what to rotate first. You can also scan across multiple projects by pulling each environment file and scanning the directory:

ggshield secret scan path .

Once you’ve identified exposed secrets, rotate them in your upstream services (like AWS, Stripe, or database providers) before updating the values in Vercel.

Harden Controls for the Next Incident

Vercel published the following guidance for customers:

  • Review the activity log for your account and environments for suspicious activity — in the dashboard or via the CLI.
  • Review and rotate environment variables. If any contain secrets (API keys, tokens, database credentials, signing keys) that were not marked as sensitive, those values should be treated as potentially exposed and rotated as a priority.
  • Take advantage of the sensitive environment variables feature going forward, so that secret values are protected from being read in the future.
  • Investigate recent deployments for unexpected or suspicious looking deployments. If in doubt, delete any deployments in question.
  • Ensure that Deployment Protection is set to Standard at a minimum.
  • Rotate your Deployment Protection tokens, if set.