How to Set Up Azure Functions: 13 Steps, 75 Min [2026]

Microsoft quietly rewrote the rulebook for its serverless platform this year. As of mid-2026, the Consumption plan that most Azure Functions tutorials still teach is officially labeled legacy in Microsoft’s own documentation, with Linux support retired outright. The replacement, Flex Consumption, is now the default recommendation for any new serverless project. If you learned Azure Functions a few years ago, or you’re starting today from a guide written in 2023, there’s a good chance you’re about to build on a foundation Microsoft is actively walking away from.

This tutorial builds a real, working project on the current stack: Azure Functions runtime 4.x, the isolated worker programming model, Node.js 22 (with Node.js 24 now generally available on the platform as of June 1, 2026), and the Flex Consumption hosting plan. You’ll stand up a serverless URL-shortener API with three functions, deploy it to a live Azure endpoint, wire up monitoring, and automate future deployments with GitHub Actions. By the end you’ll have a small but genuinely useful Azure Functions app running in the cloud, plus a mental model for how the pieces fit together.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Azure Functions Still Matters for Serverless Development in 2026

Serverless conversations tend to default to AWS Lambda, but Azure Functions runs a huge share of enterprise workloads precisely because it sits inside the Microsoft ecosystem: Entra ID authentication, Logic Apps, Cosmos DB, and the rest of the Azure portfolio all wire into it with minimal glue code, and that integration story got noticeably deeper on June 2, 2026, when Microsoft shipped general availability for hosting MCP (Model Context Protocol) apps directly on Azure Functions, alongside a preview of managed connectors reaching more than 1,400 external systems and Copilot-assisted coding and agent skills built into the authoring experience. For teams already paying for Microsoft 365 or running .NET services, Azure Functions is frequently the path of least resistance for a new API, webhook handler, or scheduled job.

2026 happens to be a forced-migration year. Microsoft flagged the first of these deadlines back in April 2025: support for runtime version 1.x ends September 14, 2026. Function apps still running the end-of-life v3 runtime on Linux Consumption get a separate, slightly later cutoff, per Microsoft’s own documentation as updated in September 2026, those apps stop running entirely on September 30, 2026, leaving runtime 4.x as the only fully supported runtime generation, with both v2 and v3 now unsupported. The in-process C# execution model loses support on November 10, 2026, a deadline Microsoft documented in April 2026, the same date it has scheduled .NET 8 LTS support on Azure Functions to close. None of that is speculative. It’s published directly on Microsoft’s Functions runtime versions page. Teams that put off the update are going to do it under deadline pressure. Teams that learn the current model now won’t have that problem.

That’s the angle this guide takes. Rather than a “hello world” that returns a string, you’ll build something with actual moving parts: an HTTP-triggered write path, an HTTP-triggered read path, persistent storage, and a scheduled cleanup job. It’s the smallest project that still touches every piece you’ll need for a production Azure Functions app.

What You’ll Build in This Azure Functions Tutorial

The project is a serverless URL shortener, the same category of app that made Bitly and TinyURL household names, scaled down to something you can build in about 75 minutes. It has three functions:

  • A POST /api/shorten HTTP-triggered function that accepts a long URL and returns a short code, storing the mapping in Azure Table Storage
  • A GET /api/{code} HTTP-triggered function that looks up the code and issues an HTTP redirect to the original URL
  • A timer-triggered cleanup function that runs on a schedule to purge expired links

Everything runs on Node.js and the Azure Functions v4 programming model, with Azure Table Storage as the only external dependency, so there’s no database server to provision separately. Once deployed, you’ll have a public HTTPS endpoint, structured logging in Application Insights, and a GitHub Actions workflow that redeploys automatically on every push to main.

Table Storage is the deliberate choice here over Cosmos DB. Cosmos DB is the more capable option, with global distribution, multiple consistency models, and a richer query language, but it’s also a service with its own pricing model that a beginner tutorial shouldn’t force on you just to store a code-to-URL mapping. Table Storage rides on the same storage account your function app already needs for its internal bookkeeping, so this project adds zero new billable resources beyond what az functionapp create provisions automatically. If you outgrow it later, the migration path to Cosmos DB’s Table API is a connection-string swap, not a rewrite.

Prerequisites: Tools, Versions, and Accounts You Need

Install and verify each of these before starting Step 1. Version numbers reflect what was current and GA at the time of writing.

ToolMinimum VersionPurpose
Node.js22 LTS (or 24)Runtime language for your functions (the only two Node.js versions currently GA on Azure Functions)
Azure CLI2.89.0 or laterProvisioning and deploying Azure resources from the terminal
Azure Functions Core Tools4.12.1 or laterLocal development, testing, and publishing (runtime 4.x host)
Visual Studio CodeLatestEditor, with the Azure Functions extension installed
Azure accountFree tier is fineHosting your function app, storage account, and Application Insights
AzuriteLatestLocal Azure Storage emulator so you don’t need a live storage account to develop
Git and a GitHub accountAny recent versionSource control and the CI/CD workflow in the final step

You don’t need prior Azure experience, but you should be comfortable with basic JavaScript and the command line. If you’ve never touched a cloud provider before, expect the whole walkthrough, including waiting on resource provisioning, to take closer to 90 minutes than 75.

Step 1: Install and Sign In to the Azure CLI

Everything you do against real Azure resources in this tutorial goes through the Azure CLI, so get it installed and authenticated first.

# macOS
brew update && brew install azure-cli

# Windows (PowerShell)
winget install -e --id Microsoft.AzureCLI

# Ubuntu/Debian
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Confirm the install and sign in
az --version
az login

az login opens a browser window for you to authenticate, then lists the subscriptions tied to your account. If you have more than one subscription, set the correct one as active before continuing:

az account set --subscription "Your Subscription Name"
az account show --output table

Confirm az --version reports 2.89.0 or newer. Older CLI builds are missing some of the Flex Consumption flags used later in this guide, and upgrading mid-tutorial is more annoying than doing it now.

Step 2: Install Azure Functions Core Tools

Core Tools gives you the func command, which handles local scaffolding, running, and publishing. Version 4.12.1 ships with Host Runtime Version 4.1048.200 bundled in, so you don’t need to install the runtime host separately. Runtime v3 is already out of support, and as of September 2026 Microsoft lists runtime 4.x as the only currently supported runtime version, so confirming you’re on 4.12.1 or later isn’t optional busywork.

# macOS
brew tap azure/functions
brew install azure-functions-core-tools@4

# Windows/Linux/macOS via npm
npm install -g azure-functions-core-tools@4 --unsafe-perm true

# Verify
func --version

You should see 4.12.1 or later. If func --version comes back with a 3.x or 2.x number, uninstall it first, since mixing Core Tools major versions on the same machine is a common source of confusing local errors later. The full changelog for every release is published on the Azure Functions Core Tools GitHub releases page, and it’s worth a skim just to see how fast this tool moves: the 4.3.0-preview1 milestone landed back on September 8, 2025, and the releases since, including what changed in 4.12.1, are the fastest way to confirm whether a bug you’re hitting locally is already fixed in a newer build.

Step 3: Install the VS Code Azure Functions Extension

You can do this entire tutorial from a plain terminal, but the VS Code extension adds inline debugging, a visual list of your deployed functions, and one-click log streaming, which is worth the two minutes it takes to set up.

  • Open VS Code and go to the Extensions panel
  • Search for “Azure Functions” and install the Microsoft-published extension (ms-azuretools.vscode-azurefunctions)
  • Click the new Azure icon in the sidebar and sign in with the same account you used for az login

Once signed in, your subscription and any existing function apps show up in the sidebar. That panel becomes the fastest way to tail logs once your app is live in Azure.

Step 4: Scaffold Your Function App Project

Create a project folder and initialize it with the JavaScript template and the v4 programming model, which is the current model for all languages other than legacy C# in-process apps.

mkdir url-shortener-functions && cd url-shortener-functions
func init --javascript --model V4
func new --name shorten --template "HTTP trigger"

That second command is the one you’ll run repeatedly as you add functions to this project. It’s worth knowing why the --model V4 flag matters: earlier versions of Azure Functions required a separate function.json file per function, hand-written to declare each trigger and binding, plus a matching code file. The v4 programming model collapses that into a single file. You declare the trigger, the route, and the handler directly in code with app.http() or app.timer(), and the framework infers the binding configuration at startup. It’s less boilerplate, easier to review in a pull request, and it’s the model every current Microsoft tutorial and template defaults to, so it’s what this guide uses throughout. As Microsoft’s own Cosmos DB engineering blog puts it when walking through the same workflow:

“Navigate into the project folder and create an HTTP trigger: func new –name MyHttpFunction –template ‘HTTP trigger'”

Microsoft DevBlogs, Building Your First Serverless HTTP API on Azure

func init generates a handful of files. host.json configures the function host itself, including which extension bundle version to load. local.settings.json holds environment variables and connection strings for local development only. It should never be committed to git, and func init already adds it to .gitignore by default. package.json tracks your npm dependencies, and everything you write lives under src/functions/.

Open host.json and confirm the extension bundle looks like this, which pins you to the 4.x compatible range of trigger and binding extensions:

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.0.0, 5.0.0)"
  }
}

Step 5: Write Your First HTTP-Triggered Function

func new already scaffolded src/functions/shorten.js for you. Microsoft’s own quickstart walks through the equivalent portal-based flow the same way:

“Under Select a template, scroll down and choose the HTTP trigger template.”

Microsoft Learn, Create your first function using the Azure portal

Replace the generated file’s contents with the code below. This version accepts a JSON body with a url field, generates a short code, and returns it, storage comes in the next step:

const { app } = require('@azure/functions');
const { customAlphabet } = require('nanoid');

const generateCode = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 7);

app.http('shorten', {
    methods: ['POST'],
    authLevel: 'anonymous',
    route: 'shorten',
    handler: async (request, context) => {
        let body;
        try {
            body = await request.json();
        } catch {
            return { status: 400, jsonBody: { error: 'Request body must be valid JSON' } };
        }

        if (!body.url || !/^https?:///i.test(body.url)) {
            return { status: 400, jsonBody: { error: 'A valid absolute url field is required' } };
        }

        const code = generateCode();
        context.log(`Generated short code ${code} for ${body.url}`);

        return {
            status: 201,
            jsonBody: { code, shortUrl: `${process.env.BASE_URL || 'http://localhost:7071'}/api/${code}` }
        };
    }
});

Install the one dependency this function needs beyond the Functions SDK itself:

npm install nanoid@3

Pin nanoid to the 3.x line — version 4 and later ship as an ES module only, which complicates a CommonJS Functions project unless you also convert your whole app to ESM.

Step 6: Run and Test Your Function Locally

Start the local host:

func start

You’ll see the host boot up and print the local URL for your function, typically http://localhost:7071/api/shorten. In a second terminal, send it a test request:

curl -i http://localhost:7071/api/shorten 
  -X POST 
  -H "Content-Type: application/json" 
  -d '{"url":"https://tech-insider.org"}'

Expected output:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{"code":"a1b2c3d","shortUrl":"http://localhost:7071/api/a1b2c3d"}

If you get a 400 back, double-check your JSON is well-formed and that the URL includes the http:// or https:// prefix. If curl can’t connect at all, confirm func start is still running in the other terminal and didn’t exit on a syntax error.

Step 7: Add Azure Table Storage for Persistent Data

Right now the function generates a code but throws it away. Fix that with Azure Table Storage, a low-cost NoSQL key-value store that pairs naturally with Functions since every function app already needs a storage account for its own bookkeeping.

For local development, install and start Azurite, Microsoft’s open-source storage emulator, instead of pointing at a live Azure account:

npm install -g azurite
mkdir azurite-data
azurite --silent --location ./azurite-data &

In local.settings.json, point AzureWebJobsStorage at the emulator instead of a real connection string:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "BASE_URL": "http://localhost:7071"
  }
}

Install the Table Storage SDK:

npm install @azure/data-tables

Now update shorten.js to write each generated code into a links table:

const { app } = require('@azure/functions');
const { customAlphabet } = require('nanoid');
const { TableClient } = require('@azure/data-tables');

const generateCode = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 7);

async function getTableClient() {
    const client = TableClient.fromConnectionString(process.env.AzureWebJobsStorage, 'links');
    await client.createTable().catch(() => {}); // no-op if it already exists
    return client;
}

app.http('shorten', {
    methods: ['POST'],
    authLevel: 'anonymous',
    route: 'shorten',
    handler: async (request, context) => {
        let body;
        try {
            body = await request.json();
        } catch {
            return { status: 400, jsonBody: { error: 'Request body must be valid JSON' } };
        }
        if (!body.url || !/^https?:///i.test(body.url)) {
            return { status: 400, jsonBody: { error: 'A valid absolute url field is required' } };
        }

        const code = generateCode();
        const tableClient = await getTableClient();

        await tableClient.createEntity({
            partitionKey: 'link',
            rowKey: code,
            targetUrl: body.url,
            createdAt: new Date().toISOString()
        });

        context.log(`Stored short code ${code} for ${body.url}`);
        return {
            status: 201,
            jsonBody: { code, shortUrl: `${process.env.BASE_URL}/api/${code}` }
        };
    }
});

Restart func start and re-run the curl test from Step 6. This time the entity actually persists, and it survives a restart of the local host because Azurite is writing to disk under ./azurite-data.

Step 8: Build the Redirect Endpoint

Scaffold the second function:

func new --name redirect --template "HTTP trigger"

Replace its contents with a route that reads the code from the URL, looks it up, and issues a 302 redirect:

const { app } = require('@azure/functions');
const { TableClient } = require('@azure/data-tables');

app.http('redirect', {
    methods: ['GET'],
    authLevel: 'anonymous',
    route: '{code}',
    handler: async (request, context) => {
        const code = request.params.code;
        const tableClient = TableClient.fromConnectionString(process.env.AzureWebJobsStorage, 'links');

        try {
            const entity = await tableClient.getEntity('link', code);
            context.log(`Redirecting ${code} -> ${entity.targetUrl}`);
            return {
                status: 302,
                headers: { Location: entity.targetUrl }
            };
        } catch (err) {
            context.log(`No entity found for code ${code}: ${err.message}`);
            return { status: 404, jsonBody: { error: 'Short code not found' } };
        }
    }
});

Note the route is just {code}, not /api/{code}. Azure Functions prefixes every HTTP route with /api by default, so declaring route: '{code}' already produces the GET /api/{code} path described earlier. Test it against the code your /api/shorten call returned:

curl -i http://localhost:7071/api/a1b2c3d

You should get a 302 with a Location header pointing back to https://tech-insider.org.

Step 9: Add a Timer-Triggered Cleanup Function

The third function demonstrates a trigger type with no HTTP request behind it at all: a CRON-scheduled job. Scaffold it:

func new --name cleanupExpiredLinks --template "Timer trigger"

Replace the generated code with a job that deletes any link older than 90 days, running once a day at 3 AM:

const { app } = require('@azure/functions');
const { TableClient } = require('@azure/data-tables');

app.timer('cleanupExpiredLinks', {
    schedule: '0 0 3 * * *',
    handler: async (myTimer, context) => {
        const tableClient = TableClient.fromConnectionString(process.env.AzureWebJobsStorage, 'links');
        const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
        let deleted = 0;

        const entities = tableClient.listEntities({
            queryOptions: { filter: `PartitionKey eq 'link'` }
        });

        for await (const entity of entities) {
            if (new Date(entity.createdAt) < cutoff) {
                await tableClient.deleteEntity(entity.partitionKey, entity.rowKey);
                deleted++;
            }
        }

        context.log(`Cleanup run complete. Removed ${deleted} expired link(s).`);
    }
});

The schedule string 0 0 3 * * * is a six-field CRON expression (second, minute, hour, day, month, day-of-week) evaluated in UTC by default. You can force the local host to trigger it immediately for testing by adding "runOnStartup": true to the timer options temporarily, but remove that before deploying, since you don't want a production cleanup job firing on every cold start.

Step 10: Provision Azure Resources with the CLI

With all three functions working locally, it's time to create the real Azure resources. Microsoft's current guidance is explicit on this point: the Functions hosting comparison page states plainly that the Consumption plan is now a legacy option and that new serverless apps should use Flex Consumption instead. So that's what this tutorial deploys to.

Create a resource group and a storage account first:

az group create --name rg-url-shortener --location eastus2

az storage account create 
  --name stlinkshort2026 
  --location eastus2 
  --resource-group rg-url-shortener 
  --sku Standard_LRS

Storage account names must be globally unique and lowercase alphanumeric only, so if stlinkshort2026 is taken, append a few random characters. Next, confirm Flex Consumption is available in your target region, since it isn't in every Azure region yet, though Microsoft pushed a wave of additional regions to general availability on May 19, 2025, and then create the function app:

# List regions that currently support Flex Consumption
az functionapp list-flexconsumption-locations --output table

# Create the function app on the Flex Consumption plan
az functionapp create 
  --resource-group rg-url-shortener 
  --name func-url-shortener-2026 
  --storage-account stlinkshort2026 
  --flexconsumption-location eastus2 
  --runtime node 
  --runtime-version 22 
  --instance-memory 2048

The --instance-memory flag is specific to Flex Consumption, which currently offers three fixed instance sizes: 512 MB, 2,048 MB, and 4,096 MB, according to Microsoft's Functions scale and hosting documentation, the 512 MB tier having arrived in preview on May 19, 2025 as the lightest-weight option for low-traffic workloads. Pick 2,048 MB unless you know your workload is either trivially light or memory-hungry. Function app names must be globally unique across all of Azure, since they map directly to a *.azurewebsites.net hostname, so func-url-shortener-2026 may need a suffix if someone beat you to it.

Step 11: Deploy Your Function App to Azure

With the resources provisioned, publishing your local code is a single command, the same one developers have relied on for years:

"We can now deploy the function with the following command: func azure functionapp publish $FUNCTION_NAME"

zachsnoek, software engineer, How to Build an HTTP-Triggered Function with Azure Functions
func azure functionapp publish func-url-shortener-2026

Core Tools zips your project, uploads it, and remotely installs your npm dependencies. Expect output similar to this:

Getting site publishing info...
Uploading package...
Uploading 4.1 MB [##################################################]
Deployment successful.
Remote build succeeded!
Syncing triggers...
Functions in func-url-shortener-2026:
    shorten - [httpTrigger]
        Invoke url: https://func-url-shortener-2026.azurewebsites.net/api/shorten
    redirect - [httpTrigger]
        Invoke url: https://func-url-shortener-2026.azurewebsites.net/api/{code}
    cleanupExpiredLinks - [timerTrigger]

Test the live endpoint exactly like you tested it locally, just swap the hostname:

curl -i https://func-url-shortener-2026.azurewebsites.net/api/shorten 
  -X POST 
  -H "Content-Type: application/json" 
  -d '{"url":"https://tech-insider.org"}'

Step 12: Configure Application Settings and Application Insights

Your live function app still needs the BASE_URL environment variable that local.settings.json provided locally, since that file never gets deployed. Set it as an app setting:

az functionapp config appsettings set 
  --name func-url-shortener-2026 
  --resource-group rg-url-shortener 
  --settings "BASE_URL=https://func-url-shortener-2026.azurewebsites.net"

The Azure CLI provisions an Application Insights resource automatically when you create a function app through az functionapp create, unless you explicitly opt out. Confirm it's linked and check that telemetry is flowing:

az functionapp config appsettings list 
  --name func-url-shortener-2026 
  --resource-group rg-url-shortener 
  --query "[?name=='APPLICATIONINSIGHTS_CONNECTION_STRING']"

If that query returns an empty array, Application Insights isn't wired up, and you'll want to create and link it manually before going further, since debugging a production function without centralized logs is painful. In the Azure portal, open your function app, go to Application Insights under Settings, and either link an existing instance or create a new one in a couple of clicks.

Step 13: Automate Deployment with GitHub Actions

Manually running func azure functionapp publish is fine for this tutorial, but any real project should deploy on every merge to main. Push your project to a GitHub repository, then get a publish profile for the workflow to authenticate with:

az functionapp deployment list-publishing-profiles 
  --name func-url-shortener-2026 
  --resource-group rg-url-shortener 
  --xml

Copy that XML output into a new repository secret named AZURE_FUNCTIONAPP_PUBLISH_PROFILE under Settings → Secrets and variables → Actions. Then add this workflow at .github/workflows/deploy.yml:

name: Deploy Azure Function App

on:
  push:
    branches: [main]

env:
  AZURE_FUNCTIONAPP_NAME: func-url-shortener-2026
  NODE_VERSION: '22.x'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Install dependencies
        run: npm ci --production

      - name: Deploy to Azure Functions
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
          package: '.'
          publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}

Commit and push, and check the Actions tab. A green check mark means the workflow authenticated correctly and redeployed your three functions without you touching the terminal.

Choosing a Hosting Plan: Consumption vs Flex Consumption vs Premium

This tutorial deploys to Flex Consumption, but it's worth understanding why, since older tutorials and Stack Overflow answers still default to plain Consumption. The numbers below come directly from Microsoft's Functions scale and hosting comparison documentation.

PlanMonthly Free GrantDefault / Max TimeoutMax InstancesBest For
Flex Consumption250,000 executions + 100,000 GB-s30 min / unbounded*1,000New serverless apps (Microsoft's current default recommendation)
Consumption (legacy)1,000,000 requests + 400,000 GB-s5 min / 10 min200 (Windows) / 100 (Linux)Existing Windows apps only (Linux Consumption is retired for new use)
PremiumNone (reserved-capacity billing)30 min / unbounded100 (Windows) / 20-100 (Linux)Always-warm instances, VNet integration, continuous traffic
Dedicated (App Service Plan)None (flat App Service rate)30 min / unbounded with Always On10-30 (100 on ASE)Predictable billing on existing App Service capacity

*Flex Consumption and Premium have no hard maximum timeout, but a 60-minute grace period applies during scale-in and a 10-minute grace period during platform updates. One limit applies no matter which plan you pick: any HTTP-triggered function has 230 seconds to respond, a ceiling set by Azure Load Balancer's default idle timeout, not by your functionTimeout setting. If a request genuinely needs longer than that, offload the work to a queue-triggered function or the Durable Functions async pattern instead of trying to hold the HTTP connection open.

The free grant math matters too. Per Azure's pricing documentation as updated in October 2026, Consumption's free tier is nominally more generous on paper, 1 million requests and 400,000 GB-s of execution time at no charge, versus Flex Consumption's 250,000 executions and 100,000 GB-s. But Flex Consumption is where Microsoft is actively investing: in August 2025 Microsoft called out long-running executions, private networking, selectable instance sizes, and per-function concurrency controls as core Flex Consumption capabilities, on top of the per-function scaling, availability zone support, and virtual network integration already built in. For a new project, that direction of travel matters more than a bigger free-tier number on a plan headed for retirement.

Premium and Dedicated plans exist for a different problem than this tutorial solves. Both keep at least one instance warm at all times, which eliminates cold starts entirely but means you're paying for idle capacity even at 3 AM with zero traffic. Microsoft's Functions scale and hosting documentation recommends Premium specifically when a function app runs near-continuously, needs more CPU or memory than the consumption tiers offer, or requires a custom Linux container image. For a low-traffic side project or an internal tool, that always-on billing model rarely pays for itself. Save it for workloads where latency-sensitive traffic is constant enough to justify the fixed cost.

Common Pitfalls When Building Azure Functions

A handful of mistakes account for most of the frustration developers hit in their first few weeks with Azure Functions. Most of them aren't obscure edge cases, they're default behaviors that surprise people coming from a traditional server or from a different serverless platform. Watch for these:

  • Committing local.settings.json to git. It's excluded by default, but if you ever force-add files or copy a project from elsewhere, double-check it isn't tracked. It can contain live connection strings.
  • Leaving authLevel as anonymous in production. Anonymous auth is fine for local testing and this tutorial's scope, but a public API with no key, rate limiting, or API Management layer in front of it is an open door once real traffic finds it.
  • Assuming functionTimeout controls HTTP requests. It doesn't, not fully. The 230-second Azure Load Balancer ceiling applies regardless of your timeout setting or hosting plan.
  • Mismatching local and deployed Node.js versions. If your machine runs Node 24 locally but you provisioned the function app with --runtime-version 22, you can hit subtle behavioral differences that only show up after deployment.
  • Ignoring the free-grant ceiling. 250,000 executions a month sounds like a lot until a misbehaving retry loop or a scraping bot finds your endpoint. Set up a budget alert in Azure Cost Management before you forget about it.
  • Skipping Application Insights until something breaks. Wiring up monitoring after an incident means you have no baseline to compare against. Confirm telemetry is flowing in Step 12, not during your first outage.
  • Testing only against Azurite and never against real Table Storage. The emulator is close to production behavior but not identical, particularly around throttling and query performance at scale. Run at least one manual test against the live storage account before you call a feature done.

Troubleshooting Azure Functions: Common Errors and Fixes

Most Azure Functions problems fall into a small number of recurring categories. Here's what to check first for each one.

SymptomLikely CauseFix
"func: command not found" after installnpm global bin directory isn't on your PATHRestart your terminal, then run npm config get prefix and add that path's bin folder to PATH
Function works locally but 404s after deployDeployment didn't sync triggers, or the function app name in the publish command is wrongRun func azure functionapp list-functions <name> to confirm what actually deployed
"Value cannot be null. Parameter name: connectionString"AzureWebJobsStorage is missing or Azurite isn't runningCheck local.settings.json locally, or az functionapp config appsettings list in Azure
Browser requests fail with a CORS errorThe function app has no CORS origins configuredRun az functionapp cors add --name <app> --resource-group <rg> --allowed-origins "https://yoursite.com"
Timer trigger fires at the wrong clock timeCRON schedules evaluate in UTC by defaultAdd a WEBSITE_TIME_ZONE app setting or recalculate the schedule in UTC
"There was an error restoring dependencies" during deploypackage.json is out of sync with installed packagesRun npm install locally before publishing, or deploy with remote build enabled
Application Insights shows no telemetryAPPLICATIONINSIGHTS_CONNECTION_STRING isn't set on the function appLink or recreate Application Insights from the portal's Settings pane
HTTP function times out around 230 seconds regardless of settingsAzure Load Balancer's fixed idle timeout, not your functionTimeoutMove long-running work to a queue trigger or Durable Functions async pattern
GitHub Actions deploy fails with an authentication errorThe publish profile secret is stale, rotated, or misnamedRegenerate it with az functionapp deployment list-publishing-profiles --xml and update the GitHub secret
"SKU not allowed" error creating a new function appThe resource group already has an incompatible plan type from a previous appCreate the new function app in a fresh, empty resource group

Advanced Tips for Production-Ready Azure Functions

Once the basic app is deployed and working, a few upgrades separate a tutorial project from something you'd trust with real traffic.

  • Switch to managed identity. Instead of a storage connection string in an app setting, grant the function app's system-assigned managed identity the Storage Table Data Contributor role and authenticate with DefaultAzureCredential. One less secret to rotate or leak.
  • Use Durable Functions for multi-step workflows. If a single request needs to call three downstream services in sequence with retries, a plain HTTP trigger gets messy fast. Durable Functions' orchestration model handles chaining, fan-out/fan-in, and long-running state without you hand-rolling a queue system.
  • Add Always Ready instances if cold starts hurt you. Flex Consumption supports pre-provisioned always-ready instances specifically to cut the latency spike on the first request after idle. It costs more per month, but for a customer-facing API it's often worth it.
  • Turn on VNet integration for anything touching a database. Flex Consumption and Premium both support private endpoints and virtual network integration. Plain Consumption does not. If your function needs to reach a private SQL server or Redis instance, that's the deciding factor in plan choice, not price.
  • Know that Flex Consumption doesn't support deployment slots. If you rely on blue-green deployments with slot swapping, that's a Premium or Dedicated plan feature only, according to Microsoft's plan comparison table. Plan your release strategy accordingly before you commit to Flex Consumption.
  • Write structured logs with correlation context. context.log() calls flow into Application Insights automatically, but tagging each log line with a request or correlation ID makes tracing a single user's request across all three functions dramatically easier during an incident.

Cleaning Up Azure Resources to Avoid Surprise Bills

Everything in this tutorial fits inside the free grants of Flex Consumption and a Standard_LRS storage account under normal testing traffic, but it's still good practice to tear resources down once you're done experimenting. Because everything lives in one resource group, cleanup is a single command:

az group delete --name rg-url-shortener --yes --no-wait

That deletes the function app, the storage account, and the linked Application Insights resource together. The --no-wait flag returns control of your terminal immediately instead of blocking until deletion finishes in the background. If you want to keep the project running as a portfolio piece instead, leave it up, just set a spending budget alert in Azure Cost Management so a traffic spike doesn't surprise you later.

Frequently Asked Questions

Is Azure Functions free to use?

Yes, within limits. As of Azure's pricing documentation updated in October 2026, the Flex Consumption plan includes 250,000 free executions and 100,000 free GB-seconds of resource consumption per month, per subscription, according to Microsoft's official Azure Functions pricing page. Beyond that grant, you pay per execution and per GB-second of memory used.

Should I use Consumption or Flex Consumption in 2026?

Flex Consumption, for any new project. Microsoft's own hosting documentation now labels Consumption a legacy plan and states that Linux Consumption support has been retired, with the option to host on Linux Consumption scheduled to disappear entirely by September 30, 2028. Consumption still works for existing Windows-based apps, but it isn't where Microsoft is investing new features.

Can I develop Azure Functions locally without an Azure account?

Yes. Azure Functions Core Tools runs the full function host on your machine, and Azurite emulates Azure Storage locally, so everything through Step 9 of this tutorial works with zero cloud dependency. You only need an Azure account starting at Step 10, when you provision real resources to deploy to.

What programming languages does Azure Functions support?

Runtime 4.x supports C# (.NET 8, 9, and 10 on the isolated worker model, with .NET 10 support added as of January 2026), though Microsoft has scheduled .NET 8 LTS support on Azure Functions to end November 10, 2026, so new projects should target .NET 9 or 10 instead. It also supports Java (8 through 25), Node.js (22 and 24), PowerShell (7.4 GA, 7.6 in preview), and Python (3.10 through 3.14, with the azure-functions package on PyPI itself reaching version 1.26.0 on August 28, 2026), per Microsoft Learn's supported language versions table. Go 1.24 and later is available in preview, but currently only on the Flex Consumption plan.

How does Azure Functions compare to AWS Lambda?

The core execution model is similar: both charge per invocation and per resource-time, both scale automatically, and both support a comparable set of trigger types. The practical differences show up in ecosystem integration and tooling. Azure Functions integrates more tightly with Entra ID and the rest of Azure. Lambda integrates more tightly with the rest of AWS. If you're weighing the two for a new project, our guide to building an AWS Lambda API walks through the equivalent workflow on AWS so you can compare the developer experience directly.

Why does my function deploy successfully but return a 404?

This almost always means trigger syncing didn't complete, or the route you're hitting doesn't match what's actually registered. Run func azure functionapp list-functions <app-name> to see exactly what Azure thinks is deployed, and confirm your route strings, remember Azure Functions automatically prefixes HTTP routes with /api unless you've customized the route prefix in host.json.

Do I need Durable Functions for a simple API like this one?

No. Durable Functions adds real value when a single logical operation spans multiple steps, retries, or needs to pause and resume state, think order-processing pipelines or approval workflows. A URL shortener's three independent functions don't need that orchestration layer, which is exactly why this tutorial doesn't use it.

What happens to my app if I exceed the free execution grant?

Your functions keep running, they don't shut off. You simply start paying the per-execution and per-GB-second rate for usage beyond the 250,000 execution and 100,000 GB-s Flex Consumption grant. That's exactly why the Common Pitfalls section above recommends setting a budget alert rather than relying on the free tier as a hard ceiling.

Marcus Chen

Marcus Chen

Gaming & Consumer Tech Editor

Marcus Chen is a senior editor at Tech Insider, where he leads coverage of the US online gaming market, including sweepstakes and social casinos, alongside consumer technology. He evaluates operators on their published terms, licensing and RNG certifications, stated redemption policies, and corroborating independent reporting, and writes plainly about what the evidence supports. Tech Insider does not run first-party money tests and does not gamble with reader funds. Marcus has reported on the technology and online-gaming industries for more than a decade.

View all articles