DEV Community

Cover image for Local Development Setup: Tools, Debugging, and Hot Reload
Martin Oehlert
Martin Oehlert

Posted on

Local Development Setup: Tools, Debugging, and Hot Reload

Azure Functions for .NET Developers: Series


You add a breakpoint. You press F5. The function executes. The breakpoint never fires.

This is the most common introduction to Azure Functions local development. The reason is non-obvious: when you debug a .NET isolated worker function, two separate processes run. Your debugger attached to the host process (func.exe) instead of the worker process (dotnet.exe), which is where your code actually executes.

local.settings.json causes a different kind of confusion. Unlike appsettings.json, it injects environment variables: put a connection string in the wrong section and your bindings silently break. Azurite is a related trap: timer, queue, and blob triggers all use Azure Storage internally, so the emulator has to be running before those trigger types will initialize, even if your function code touches no storage directly.

This covers the full local setup for Azure Functions with .NET 10 and the isolated worker model. What's not here: deployment, Application Insights (that's Part 9), or unit testing (Part 7).


What you need: three installs

Three things have to be installed and working before you can run any Azure Function locally.

A. .NET 10 SDK

dotnet --version   # expect 10.x.x
dotnet --list-sdks # confirm 10.x is listed
Enter fullscreen mode Exit fullscreen mode

If you followed Parts 1-3, this is already done. If not, download from dot.net.

B. Azure Functions Core Tools v4

Core Tools provides the func CLI that starts the local Functions host. Install it with your package manager:

  • macOS: brew tap azure/functions && brew install azure-functions-core-tools@4
  • Windows: winget install Microsoft.Azure.FunctionsCoreTools or the MSI download (64-bit required for VS Code debugging)
  • Linux: APT install from Microsoft package feeds (see the official instructions)
  • npm (cross-platform): npm i -g azure-functions-core-tools@4 --unsafe-perm true

A note on the npm option for Windows: if you are using Microsoft.Azure.Functions.Worker.Sdk 2.x, which enables dotnet run support, the npm Core Tools installation does not work correctly for that workflow. Use the winget or MSI install instead.

func --version   # expect 4.x.x
Enter fullscreen mode Exit fullscreen mode

C. Azurite

Azurite emulates Azure Blob, Queue, and Table storage locally. Install it once; you will need it running for every non-HTTP function.

  • VS Code extension (recommended for VS Code users): search "Azurite" in the Extensions panel, or install Azurite.azurite directly
  • npm: npm install -g azurite
  • Docker: docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
  • Visual Studio 2026: Azurite is bundled and starts automatically when you press F5

The old Azure Storage Emulator is deprecated. If you see references to it in older articles or documentation, ignore them.

Verification

Run this sequence to confirm everything is wired up correctly:

dotnet --version
func --version
# Start Azurite: either the VS Code extension (Command Palette: "Azurite: Start") or `azurite` in a terminal
func init TestProj --worker-runtime dotnet-isolated --target-framework net10.0
cd TestProj
func start
# Should print: "Host lock lease acquired" with no errors
Enter fullscreen mode Exit fullscreen mode

If you see SocketException: Unable to connect to the remote server instead of the lock lease message, Azurite is not running. Start it first, then retry func start.


The two-process model

This is the section most local dev tutorials skip, which is why "my breakpoint won't fire" is such a common question.

In the isolated worker model, two separate processes run every time you start a debug session:

┌──────────────────────────────────────────┐
│   func.exe  (the Functions host)         │
│   - Manages triggers                     │
│   - Handles bindings                     │
│   - Routes HTTP requests                 │
│   - Runs on port 7071                    │
└──────────────────────┬───────────────────┘
                       │ gRPC
                       │
┌──────────────────────┴───────────────────┐
│   dotnet.exe  (your worker)              │
│   - Runs your actual function code       │
│   - Gets invoked by the host via gRPC    │
│   - This is where breakpoints fire       │
└──────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

func.exe is the Functions runtime: it manages triggers, handles bindings, and routes incoming requests. dotnet.exe is your application: it receives invocation requests from the host over gRPC and runs your actual function code. Breakpoints live in dotnet.exe, not func.exe.

This matters for debugging in two ways. First, your debugger must attach to the worker process (dotnet.exe), not to func.exe. This is why the launch.json the Azure Functions extension generates uses "request": "attach" rather than "request": "launch", and why the process ID is set to ${command:azureFunctions.pickProcess} instead of a static value. Second, the two processes produce separate log streams with separate configuration: host.json controls what func.exe logs, while your ILogger<T> calls are configured on the worker side. Changing one does not affect the other.

For comparison: the deprecated in-process model runs everything in a single process. One process, one debugger attach target. That simplicity is gone with the isolated model, but the isolation is what allows it to run on .NET 10 instead of being locked to .NET 8.


local.settings.json: not config, environment variables

A common mental model for local.settings.json is that it works like appsettings.json. It does not.

Everything in the Values section is read by Core Tools at startup and injected as process environment variables into both func.exe and the worker process. That is the entire job of this file when running locally. In Azure, there is no local.settings.json at all: the App Settings page in the portal (or the equivalent in Bicep/ARM/Terraform) sets those same environment variables directly on the hosted function app.

File structure:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "MyQueueConnection": "UseDevelopmentStorage=true",
    "MyServiceBusConnection": "<real-sb-connection-string>"
  },
  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "*",
    "CORSCredentials": false
  },
  "ConnectionStrings": {
    "AppDb": "Server=localhost;Database=MyApp;Trusted_Connection=True;"
  }
}
Enter fullscreen mode Exit fullscreen mode

Two keys in Values are required for every project:

  • FUNCTIONS_WORKER_RUNTIME: must be "dotnet-isolated" (not "dotnet", which is the in-process value; using it causes confusing startup failures)
  • AzureWebJobsStorage: required for every trigger type except HTTP. The host uses this storage account for timer leases, key management, and Durable Functions. Set to "UseDevelopmentStorage=true" when Azurite is running.

The ConnectionStrings section gotcha

ConnectionStrings is for Entity Framework and similar frameworks that call IConfiguration.GetConnectionString("Name"). It is not for Functions trigger and binding configuration. If you put your Service Bus or Queue connection string here, the binding's Connection property won't find it, and the error message won't point you at the right place. All binding connection strings must go in Values.

Why the file is gitignored

Even if your local values only point at Azurite today, a real connection string will appear in this file at some point, whether you add it or a teammate does. Once committed, it lives in git history. The .gitignore and .funcignore that func init generates both exclude local.settings.json from the start.

Sharing the structure with the team

The standard approach: commit a local.settings.json.example file with the same structure but placeholder values. New team members copy it to local.settings.json and fill in the real values.

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "ServiceBusConnection": "<your-service-bus-connection-string>"
  }
}
Enter fullscreen mode Exit fullscreen mode

Document in the README which values work with Azurite and which require a real Azure service. Your teammates will thank you the first time they clone the repo.

Reading values in code

Direct access anywhere in your code:

var value = Environment.GetEnvironmentVariable("MyKey");
Enter fullscreen mode Exit fullscreen mode

The recommended approach: inject IConfiguration and read by key name. Since the values are environment variables, the key name is the exact string in Values:

public class OrderProcessor
{
    private readonly IConfiguration _config;

    public OrderProcessor(IConfiguration config)
    {
        _config = config;
    }

    public void Process()
    {
        var connStr = _config["MyQueueConnection"];
    }
}
Enter fullscreen mode Exit fullscreen mode

Part 6 covers structuring values for testability with IOptions<T>, including the double-underscore separator needed for nested configuration in environment variables.


Azurite: your local storage account

Azurite emulates the three Azure Storage services locally: Blob, Queue, and Table. Azure Functions uses these services internally, which is why Azurite has to be running even if your function code does not touch storage directly.

Why the host needs storage

The Functions host uses Azure Storage for:

  • Timer trigger lease management: prevents duplicate executions when multiple instances run
  • Function key storage: host keys and function keys are persisted in blob storage
  • Durable Functions task hub: state and message coordination
  • Event Hubs checkpoints: tracks which events have been processed

AzureWebJobsStorage in local.settings.json is the connection string for this internal storage use. Only pure HTTP-only projects can skip it. Everything else needs Azurite running first.

Which service maps to which trigger

Azurite service Port Used by
Blob service 10000 Blob trigger, Blob input/output bindings, host key storage
Queue service 10001 Queue trigger, Queue output binding, Durable Functions activity queue
Table service 10002 Table input/output bindings, Durable Functions instance state

Azurite Table Storage is still in preview as of February 2026. It works for most local development scenarios but is not production-equivalent.

You can start individual services if you want to be selective: azurite-blob, azurite-queue, or azurite-table as separate commands. From the VS Code Command Palette: Azurite: Start Blob Service, Azurite: Start Queue Service, etc.

Connection strings

The shorthand that works when Azurite runs on the default ports:

UseDevelopmentStorage=true
Enter fullscreen mode Exit fullscreen mode

The full explicit form, needed when Azurite is in Docker or using non-default ports:

DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;
Enter fullscreen mode Exit fullscreen mode

The devstoreaccount1 account name and the key above are the public well-known Azurite credentials, the same for every developer. They carry no security value.

Common gotchas

  • Port conflicts: if ports 10000, 10001, or 10002 are already in use, Azurite fails silently or with a cryptic error. Check with lsof -i :10000 (macOS/Linux) or netstat -ano | findstr :10000 (Windows). Run with --blobPort 20000 --queuePort 20001 --tablePort 20002 to use alternate ports, and update your connection strings accordingly.

  • Data location: running azurite with no --location flag writes data files to the current working directory. The VS Code extension stores data in the workspace folder. Run Azurite: Clean from the Command Palette to wipe all data and start fresh, which is useful when you want to test a clean startup sequence.

  • Docker networking: UseDevelopmentStorage=true resolves to 127.0.0.1, meaning the local machine. If your function host runs in a Docker container, 127.0.0.1 points inside that container, not at Azurite running on your host. Use host.docker.internal in the explicit connection string form instead.

  • Never publish UseDevelopmentStorage=true to Azure: the Functions host will fail to start. Azure App Settings needs a real storage account connection string, not the Azurite shorthand.


Debugging in VS Code

Required extensions

Two extensions are needed:

  • Azure Functions (ms-azuretools.vscode-azurefunctions): provides the ${command:azureFunctions.pickProcess} variable that makes F5 work, and the Execute Function Now... command for triggering non-HTTP functions from the editor
  • C# Dev Kit (ms-dotnettools.csdevkit): the recommended C# extension for new setups. Installs the base C# extension (ms-dotnettools.csharp) automatically, which provides the coreclr debugger needed for attaching to the worker process.

Recommended additions:

  • Azurite (Azurite.azurite): start and stop Azurite directly from the Command Palette without switching to a terminal
  • REST Client (humao.rest-client): run .http files against your functions from inside the editor

The F5 workflow

When you press F5 on a Functions project, the following sequence runs:

  1. VS Code executes the build (functions) task from tasks.json (a dotnet build with --no-incremental)
  2. The Azure Functions extension starts func start
  3. func.exe launches the worker process (dotnet.exe)
  4. The extension resolves the worker PID via ${command:azureFunctions.pickProcess}
  5. The coreclr debugger attaches to the worker process
  6. Breakpoints activate

VS Code debug session paused at a breakpoint, with the func host running in the terminal and .NET worker threads visible in the Call Stack panel

Generated launch.json

Running func init for a dotnet-isolated project generates this configuration:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to .NET Functions",
      "type": "coreclr",
      "request": "attach",
      "processId": "${command:azureFunctions.pickProcess}"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

"request": "attach" is not a mistake. The worker process is started by func.exe, not by VS Code, so VS Code cannot launch it. It can only attach to it after the fact. The ${command:azureFunctions.pickProcess} variable is what prompts the process picker if the extension cannot auto-detect the right PID.

tasks.json

The extension generates five tasks. The critical one is the func: host start task at the bottom:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "clean (functions)",
            "command": "dotnet",
            "args": [
                "clean", "${workspaceFolder}/MyProject.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "type": "process",
            "problemMatcher": "$msCompile"
        },
        {
            "label": "build (functions)",
            "command": "dotnet",
            "args": [
                "build", "${workspaceFolder}/MyProject.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "type": "process",
            "dependsOn": "clean (functions)",
            "group": { "kind": "build", "isDefault": true },
            "problemMatcher": "$msCompile"
        },
        {
            "type": "func",
            "label": "func: host start",
            "command": "host start",
            "options": {
                "cwd": "${workspaceFolder}/bin/Debug/net10.0"
            },
            "isBackground": true,
            "dependsOn": "build (functions)",
            "problemMatcher": "$func-dotnet-isolated-watch"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

The func task type points cwd at the compiled output directory because the isolated worker is an executable that runs from there. The problemMatcher is $func-dotnet-isolated-watch (not $func-dotnet-watch, which is for in-process). The dependsOn chain means F5 triggers a build before starting the host.

If you create a project via func init on the CLI instead of through the VS Code extension, you may not get .vscode files automatically. Run Azure Functions: Initialize Project for Use with VS Code from the Command Palette to generate them.

Setting breakpoints and inspecting state

Click the gutter (left of line numbers) to set a breakpoint. When the function is triggered, execution pauses. From there:

  • Variables panel: all locals and parameters in scope
  • Hover over any identifier: shows its current value inline
  • Debug Console: evaluates any C# expression against live state (LINQ works, method calls work)
  • Call Stack panel: the full call chain from the gRPC dispatcher into your function code

VS Code Debug Console evaluating a C# expression against live function state

Testing non-HTTP functions from VS Code

Command Palette → Azure Functions: Execute Function Now... → select Local project → pick the function.

This posts to the admin endpoint: POST http://localhost:7071/admin/functions/{FunctionName} with {"input": ""}. For queue triggers, set input to the message body you want to inject.

Common pitfall

Opening a subfolder of your project instead of the folder containing host.json causes F5 to fail without a clear error. The .vscode folder is not found. Always open the project root.


Debugging in Visual Studio 2026

Prerequisites

Visual Studio 2026 (GA since November 2025) is required for .NET 10 development. Visual Studio 2022 cannot target net10.0. Install Visual Studio 2026 from the standard Visual Studio installer.

Install the Azure development workload during setup or add it afterward via the Visual Studio Installer. This adds the Functions project templates and Core Tools integration. Update Core Tools separately when Visual Studio prompts, or through Tools | Options | Projects and Solutions | Azure Functions | Check for Updates.

The F5 workflow

  1. Visual Studio builds the project
  2. Visual Studio launches func.exe (output visible in the terminal pane)
  3. func.exe starts the worker (dotnet.exe) and reports the worker PID back over gRPC
  4. Visual Studio automatically attaches its debugger to the worker dotnet.exe process

No launch.json is needed. Visual Studio handles the two-process attach automatically.

Useful VS-specific features

  • App Settings dialog: right-click the project → PublishHostingManage Azure App Service settings. It shows a side-by-side view of local local.settings.json values vs. Azure App Settings, and lets you push individual values to Azure with one click, useful for keeping environments in sync without manually copying strings.
  • Remote debugging: attach to a deployed function app from Visual Studio. Requires Premium or App Service plan (not Consumption or Flex), Windows OS, and a Debug build. Available for 48 hours before it auto-disables. Use Attach to Process, set the connection type to Microsoft Azure App Services, then select dotnet.exe in the process list.

Known issue

In some Core Tools versions, starting with debugging can take 60 seconds to attach while starting without debugging is instant. Updating Core Tools via Tools | Options | Azure Functions resolves this in most cases.


Debugging in Rider

Plugin requirement

The Azure Toolkit for Rider is not bundled with Rider and is not part of the JetBrains marketplace defaults. Install it from Settings | Plugins | Marketplace, search for "Azure Toolkit for Rider". The current version is v4.6.5 (November 2025), a full rewrite of v3.x. Existing v3.x run configurations are not forward-compatible.

The run configuration trap

When you open a Functions project, Rider generates two run configurations:

  1. .NET Launch Settings Profile: based on launchSettings.json. This does not work for Azure Functions. Ignore it.
  2. Azure Function Host: the correct one. Rider calls func.exe with the right flags internally.

Rider run configuration dialog showing Azure Function Host and .NET Launch Settings Profile entries

If you see "Broken configuration due to unavailable plugin" after installing or updating to v4.0, delete the old configuration and create a new "Azure Function Host" configuration from scratch via Run | Edit Configurations | + | Azure Function Host.

Debugging workflow

Select the Azure Function Host configuration and press Shift+F9. Rider:

  1. Builds the project
  2. Starts func.exe with --dotnet-isolated-debug
  3. Reads the worker PID from the host's stdout
  4. Attaches the Rider debugger to the worker process

Each [Function] method gets gutter icons: the bug icon starts that function in debug mode, and the play icon opens a .http scratch file with an invocation request ready to send from Rider's built-in HTTP client.

Rider gutter icons on a function method — bug icon for debug, play icon for HTTP scratch file

Known bugs

  • "Azure Functions host did not return isolated worker process id": Rider reads the PID from the host log output. If the logging level in host.json is set above Information, the PID line is suppressed and Rider cannot detect the process. Fix: set "logging": { "logLevel": { "default": "Information" } } in host.json. JetBrains has marked this as wontfix upstream: the PID must come from the host log line.

  • Slow or failed debugger attach: start without debugging via the Run button, then attach manually through Run | Attach to Process and select the worker dotnet process.

  • Port configuration: the Azure Function Host run configuration does not read the port from launchSettings.json. Set the port directly in Run | Edit Configurations | Function host arguments. The default 7071 is fine for most setups.


Hot reload and dotnet watch

Hot reload support for Azure Functions isolated worker depends on which tool starts the session:

Start method What happens on save
Visual Studio F5 True hot reload (Edit and Continue). Most method body changes apply without restart.
dotnet watch Full process restart. All in-memory state is lost.
func start (and Rider's Azure Function Host config) No change detection. Stop and restart manually.
VS Code F5 Full process restart when you stop and re-run.

Visual Studio hot reload

Changes that apply without a restart:

  • Code changes inside existing method bodies
  • Adding new methods, properties, or fields to existing types

Changes that require a full restart (rude edits):

  • Adding a new trigger function (new [Function] attribute)
  • Changing binding attributes ([QueueTrigger], [BlobTrigger], etc.)
  • Changing Program.cs startup code
  • Changes to host.json or local.settings.json
  • Adding new types or changing method signatures

The binding attribute restriction is the most relevant one for daily Functions development: any time you add a new trigger or modify a binding, you need a full restart.

One gotcha: mixed-mode debugging (the "Enable native code debugging" checkbox in project properties) breaks hot reload. If hot reload stopped working after enabling that option, that's why.

dotnet watch

With Microsoft.Azure.Functions.Worker.Sdk 2.0.0+, dotnet run is supported when Core Tools is installed. dotnet watch wraps dotnet run and restarts the process on file changes:

dotnet watch
Enter fullscreen mode Exit fullscreen mode

This is a full restart, not hot reload. Useful if you want automatic restarts without manually stopping and restarting func start, but in-memory state does not survive between restarts. On Windows, requires the MSI or winget Core Tools installation (not npm).

The --dotnet-isolated-debug flag

func start --dotnet-isolated-debug
Enter fullscreen mode Exit fullscreen mode

This flag starts the worker process, pauses it immediately, and prints:

Azure Functions .NET Worker (PID: 28664) initialized in debug mode. Waiting for debugger to attach...
Enter fullscreen mode Exit fullscreen mode

The process stays paused until you manually attach a debugger. Use it when:

  • You need to debug Program.cs startup code before any function gets called (the worker pauses before startup completes, so you can attach and step through the entire initialization sequence)
  • VS Code or Rider is not auto-attaching to the correct process

For typical VS Code or Rider development, you do not need this flag. The IDE extensions handle process detection automatically.


Testing non-HTTP triggers locally

Every locally running Functions host exposes an admin API. Any trigger type can be invoked through it, without waiting for the actual trigger to fire.

The admin endpoint

# Timer trigger (input can be empty)
curl -X POST http://localhost:7071/admin/functions/CleanupFunction \
  -H "Content-Type: application/json" \
  -d '{"input": ""}'

# Queue trigger (input = the queue message body)
curl -X POST http://localhost:7071/admin/functions/OrderProcessor \
  -H "Content-Type: application/json" \
  -d '{"input": "{\"orderId\": \"ord-123\", \"amount\": 99.99}"}'

# Blob trigger
curl -X POST http://localhost:7071/admin/functions/ImageProcessor \
  -H "Content-Type: application/json" \
  -d '{"input": "test-blob-content"}'
Enter fullscreen mode Exit fullscreen mode

The function name in the URL is the string in [Function("...")], not the C# method name.

Simulating real storage events

The admin endpoint is fast for testing, but it bypasses the actual trigger mechanism. For testing the trigger polling behavior, use Azurite directly:

  • Add a message to an Azurite queue: the queue trigger picks it up on the next poll cycle (default: 2 seconds locally)
  • Upload a blob to an Azurite container: the blob trigger fires

Azure Storage Explorer (the desktop app) and the Azure Storage VS Code extension both work against Azurite. Point them at http://127.0.0.1:10000 and use the well-known devstoreaccount1 credentials.

.http scratch files

Create requests.http in the project root for quick invocations during development:

### Trigger cleanup manually
POST http://localhost:7071/admin/functions/CleanupFunction
Content-Type: application/json

{"input": ""}

### Test HTTP endpoint
POST http://localhost:7071/api/orders
Content-Type: application/json

{
  "orderId": "ord-123",
  "customerId": "cust-456",
  "amount": 99.99
}
Enter fullscreen mode Exit fullscreen mode

REST Client (VS Code), Rider's built-in HTTP client, and Visual Studio's native .http editor all execute these files with a click.


Productivity tips

Two log streams, two configs

In the isolated worker model, logging is split across two configuration points:

  • host.json: controls the Functions host logs (trigger polling, execution lifecycle, binding errors)
  • Worker-side (appsettings.json or Program.cs): controls your ILogger<T> calls

Changes to one do not affect the other. To see your Information-level logs during local development without drowning in host noise:

// host.json
{
  "version": "2.0",
  "logging": {
    "logLevel": {
      "default": "Information",
      "Host.Aggregator": "Trace",
      "Host.Results": "Information"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "MyFunctionApp": "Debug"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Application Insights drops worker logs by default

If you add Application Insights to a local setup and your ILogger calls stop appearing, this is why: Application Insights registers a default filter rule in the isolated worker model that drops Information and Debug logs. To remove it, update Program.cs:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = FunctionsApplication.CreateBuilder(args);

builder.Services
    .AddApplicationInsightsTelemetryWorkerService()
    .ConfigureFunctionsApplicationInsights();

// Application Insights registers a default filter rule that drops everything
// below Warning. Remove it to allow Information-level logs through.
builder.Logging.Services.Configure<LoggerFilterOptions>(options =>
{
    LoggerFilterRule? defaultRule = options.Rules.FirstOrDefault(rule =>
        rule.ProviderName
        == "Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider");

    if (defaultRule is not null)
    {
        options.Rules.Remove(defaultRule);
    }
});

builder.Build().Run();
Enter fullscreen mode Exit fullscreen mode

If you prefer to avoid touching Program.cs, you can configure Application Insights log levels through appsettings.json instead. Add this file to your project root (set "Copy to Output Directory" to "Copy if newer"):

{
  "Logging": {
    "LogLevel": { "Default": "Information" },
    "ApplicationInsights": {
      "LogLevel": { "Default": "Information" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When using FunctionsApplication.CreateBuilder, this file is loaded automatically and the filter removal code above is not needed.

Use APPLICATIONINSIGHTS_CONNECTION_STRING, not the instrumentation key

Microsoft ended instrumentation key ingestion support on March 31, 2025. For any project started after that date, use the connection string setting in local.settings.json:

"APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=...;IngestionEndpoint=https://..."
Enter fullscreen mode Exit fullscreen mode

Do not set both. If both are present, newer SDKs ignore the key.


Override log levels without restarting

Use double-underscore environment variable overrides in local.settings.json:

"AzureFunctionsJobHost__logging__logLevel__default": "Debug"
Enter fullscreen mode Exit fullscreen mode

This overrides host.json log levels without touching the file or restarting func start. Useful for temporarily enabling verbose output to track down a specific issue.

host.json queue settings for faster debugging

{
  "extensions": {
    "queues": {
      "batchSize": 1,
      "visibilityTimeout": "00:00:05",
      "maxDequeueCount": 3
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • batchSize: 1: processes one queue message at a time, making it much easier to follow execution in the logs
  • visibilityTimeout: "00:00:05": failed messages reappear in 5 seconds instead of the default 10 minutes, so you can iterate on error handling without waiting
  • maxDequeueCount: 3: messages move to the poison queue after 3 failures (default is 5)

Terminal aliases

# Add to .zshrc / .bashrc
alias fstart='func start'
alias fstartd='func start --dotnet-isolated-debug'
alias fstartv='func start --verbose'

# Start Azurite in background, then start the function host
alias devstart='azurite --silent --location /tmp/azurite & func start'
Enter fullscreen mode Exit fullscreen mode

What's next

You can now run any trigger type locally, attach a debugger to your actual function code, test non-HTTP functions without waiting for real messages, and reproduce storage-related behaviors against Azurite without touching a live Azure subscription.

Part 5 goes deeper into the architecture you've been running locally: the isolated worker model. Why Microsoft created a separate worker process, what you gain over the old in-process model, and what the November 2026 end-of-support deadline means for existing projects.


Azure Functions for .NET Developers: Series

Top comments (0)