A healthcare billing team once sent me a panicked message. A developer had tested a new queue-processing function by deploying it straight to the shared dev environment, again and again, to see what happened. Each failed attempt pushed bad messages into a live queue, and the poison-message count climbed past 3,000. A one-line bug cost half a day of cleanup and a surprisingly large log bill.
The fix was not more careful deployments. It was learning how to test Azure Function locally so that mistakes happen on a laptop, not in the cloud. Local testing gives you fast feedback, no deployment wait, and no risk to shared resources. It only works well if you also handle storage, settings, and identity properly, which is where most teams stumble.
In this guide, you will set up a local Azure Functions environment, run HTTP and timer triggers, emulate storage with Azurite, write automated tests, connect to Azure services without storing secrets, and troubleshoot the errors that appear on the way.
Understand What “Local” Means for Azure Functions
An Azure Function runs inside the Functions host, a runtime process that discovers your functions, wires up triggers and bindings, and calls your code. A trigger is the event that starts a function, such as an HTTP request, a timer, or a queue message.
A binding is a declarative connection to data, such as reading a blob or writing to a queue. See what Azure Functions is if you want the basics first.
When you run locally, the same host runs on your machine through Azure Functions Core Tools, a command-line package. Your code behaves almost the same as in Azure, with a few differences:
- Storage comes from an emulator, not a real Azure Storage account.
- App settings come from a file called
local.settings.json, not from the portal. - HTTP authorization keys are not enforced locally.
- Managed identities do not exist on your laptop, so you sign in as yourself.
Knowing these gaps matters because local success does not prove cloud success. I treat local testing as the first of three layers: local run, a shared dev deployment, and then production.
Pro Tip: In my experience, I write down the differences between local and cloud for each project in the README. New developers stop asking “why does it work here but not there?” within a week.
Step 1: Install the Tools
You need four things: Core Tools, a language runtime, an editor, and a storage emulator. Install Core Tools first. This guide covers Azure Functions Core Tools in detail, and these steps show how to install Core Tools for Visual Studio Code.
Check that the install worked:
func --versionThis prints the Core Tools version. Use version 4.x for the current Functions runtime. If the command is not found, restart your terminal and check your PATH. For install problems, see the fixes for Core Tools not installing on VS Code and the error you must have Azure Functions Core Tools installed.
Also install the Azure CLI, since you will use it for authentication. Start with this Azure CLI login guide. Add the Azure Functions extension to VS Code if you use it as your editor. You can also build and run from Visual Studio, as shown in how to create Azure Functions in Visual Studio.
Pro Tip: I pin the Core Tools version in the team’s setup script. A teammate on an older version once saw different binding behavior, and it took us an hour to find out why.
Step 2: Create a Sample Project
Create a small project that you can test end to end. This example builds an order-status API with a .NET isolated worker function.
func init OrderStatusApi --worker-runtime dotnet-isolated --target-framework net8.0
cd OrderStatusApi
func new --name GetOrderStatus --template "HTTP trigger" --authlevel functionfunc init creates the project folder and settings files. --worker-runtime dotnet-isolated selects the isolated worker model, where your code runs in a separate process from the host. func new adds a function named GetOrderStatus from the HTTP trigger template. --authlevel function means the deployed function will require a function key. Locally, that key is not checked, so remember to test auth in Azure.
Here is a simple function body:
using Microsoft.Azure.Functions.Worker;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
public class GetOrderStatus
{
private readonly ILogger<GetOrderStatus> _logger;
public GetOrderStatus(ILogger<GetOrderStatus> logger)
{
_logger = logger;
}
[Function("GetOrderStatus")]
public IActionResult Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{orderId}/status")] HttpRequest req,
string orderId)
{
if (string.IsNullOrWhiteSpace(orderId))
{
return new BadRequestObjectResult("orderId is required.");
}
_logger.LogInformation("Status requested for order {OrderId}", orderId);
return new OkObjectResult(new { orderId, status = "Shipped" });
}
}The [Function] attribute names the function. The [HttpTrigger] attribute sets the allowed method and a route with an orderId parameter. The code validates the input and returns JSON. For richer designs, see how to create an API with Azure Functions and the HTTP trigger guide.
If you prefer Python, the same ideas apply, and you can follow this tutorial on Azure Functions in Python.
Pro Tip: I validate route and query inputs in every HTTP function from day one. A local test with a bad input takes ten seconds and finds the crash before a customer does.
Step 3: Configure local.settings.json Safely
The local.settings.json file holds app settings and connection strings for local runs. It is not deployed to Azure. Here is a safe starting point:
json{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"OrdersTableName": "orders-dev"
},
"Host": {
"LocalHttpPort": 7071,
"CORS": "http://localhost:3000"
}
}UseDevelopmentStorage=true points the host at the local storage emulator. FUNCTIONS_WORKER_RUNTIME tells the host which language worker to start. OrdersTableName is a custom setting that your code can read as an environment variable.
CORS allows a local front end on port 3000 to call the function, which matters when you test a web app or a SharePoint web part against it. See how to access app settings in Azure Functions and the notes on environment variables.
Security warning: Never commit local.settings.json with real secrets. The Core Tools template adds it to .gitignore, but check that yours does too. Do not paste connection strings with account keys into this file, share it in chat, or save it in a spreadsheet.
If you need a secret, read it from Azure Key Vault at runtime using your own signed-in identity, as explained later. Follow these Azure Key Vault best practices for access and rotation.
Commit a safe template instead, named local.settings.sample.json, with placeholder values. New developers copy it and fill in their own.
Pro Tip: I add a pre-commit check that fails if a file named local.settings.json is staged. It has caught a real storage key before it reached the repo.
Step 4: Start Azurite for Storage Emulation
Most triggers need a storage account, because the host uses it to track state. Timer triggers, queue triggers, and blob triggers all depend on it. Azurite is Microsoft’s open-source emulator for Blob, Queue, and Table storage, and it runs on your machine.
npm install -g azurite
azurite --silent --location ./.azurite --debug ./.azurite/debug.logThe first command installs Azurite globally with npm. The second starts it, keeping data in a local .azurite folder (--location) and writing debug output to a log (--debug). Add .azurite to .gitignore. Leave it running in its own terminal.
Why use an emulator instead of a real storage account? Local runs are free, fast, and isolated, so a bad test cannot touch real data. The trade-off is that the emulator may not match every cloud feature, such as some access tiers and network rules.
For features the emulator lacks, use a small dev storage account, which I cover below. You can inspect emulator data with Azure Storage Explorer, which connects to local Azurite too.
Pro Tip: I keep Azurite data in a project folder, not the default location. When a teammate says “your messages are in my queue,” I know their emulator is shared across projects.
Step 5: Run and Call the Function
Start the host from the project folder:
func startThe host builds your project, starts the worker, and prints the endpoints. For the sample, you will see a URL like http://localhost:7071/api/orders/{orderId}/status. Use --verbose for more detail, and --port 7072 if the default port is busy.
Call it with curl:
curl -i http://localhost:7071/api/orders/10042/status-i prints response headers along with the body. You should see HTTP 200 and JSON for order 10042. Try a bad route too, then a missing resource, to see how your error handling behaves. For more run options and troubleshooting, see how to run multiple Azure Functions locally.
If the host does not start, you may see “no Functions runtime available” or “runtime is unreachable.” Check the guides on there is no functions runtime available and Azure Functions runtime is unreachable. Usually the cause is a missing runtime, a wrong worker setting, or an emulator that is not running.
Pro Tip: I always test one unhappy path before I call a function done. If I only run the happy path locally, production finds the second path for me.
Step 6: Test Non-HTTP Triggers
HTTP functions are easy to call, but timers and queues need a different approach. Core Tools exposes an admin endpoint that lets you invoke any function manually.
curl -X POST http://localhost:7071/admin/functions/NightlyCleanup \
-H "Content-Type: application/json" \
-d '{ "input": "" }'-X POST sends a POST request. The path /admin/functions/<function-name> invokes the function named NightlyCleanup. The JSON body must contain an input property, even if it is empty. The host runs the function once, right now, without waiting for the schedule. This is perfect for timer triggers. To learn schedules, see this tutorial on creating a timer trigger.
For queue triggers, put a test message into the local queue using Storage Explorer or a short script, and watch the function pick it up. Make the code idempotent, which means that running it twice with the same message gives the same result as running it once.
Queue triggers retry failed messages, so idempotency prevents duplicate orders, duplicate emails, and double charges. The overview on how to trigger Azure Functions compares trigger types, and bindings show how to read and write data declaratively.
If a deployed function never fires, but the local one works, see this guide on an Azure Function not triggering.
Pro Tip: I test every queue function with a deliberately broken message. Seeing it land in the poison queue locally is much better than finding out in production.
Step 7: Debug With Breakpoints
Running the function is half the job. Attaching a debugger lets you step through code and inspect variables. In VS Code, open the project, press F5, and choose to start the Functions host with the debugger attached.
The Azure Functions extension generates the launch and task files for you. Set a breakpoint inside Run, call the endpoint with curl, and the editor will stop at your line.
For a full walkthrough, follow this guide on how to debug an Azure Function locally, and learn how to edit an existing function in VS Code or create a function in VS Code.
Use structured logging while you debug. Write messages with named placeholders like {OrderId} instead of string concatenation, so you can search them later. See the guides on ILogger in Azure Functions and Azure Function logging. Dependency injection keeps your code testable, as shown in Azure Function dependency injection.
Pro Tip: I use a conditional breakpoint with the order ID of a failing customer. It skips thousands of calls and stops