blog
Ottorino Bruni  

How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2: Configuration, Retries and Smart Alerts

Introduction

In Part 1 of this series, I showed how to build a simple uptime monitoring tool using Azure Functions, Logic Apps, and .NET all running on the Azure free tier.

With just a few lines of C# code and a basic Logic App, we created a serverless solution that checks whether a website or API is reachable and sends an email alert if it’s not.

That initial version worked well as a minimal prototype, but it had some obvious limitations:

  • Hardcoded URL and schedule
  • Static email content
  • No retry logic
  • Non-optimal use of HttpClient

In this second part, we’ll improve and clean up our implementation by:

  • Moving configuration into environment settings
  • Sending dynamic information to the Logic App
  • Adding simple retry logic without external libraries
  • Refactoring HttpClient usage to follow best practices

Let’s make this monitor smarter, more flexible, and ready for real-world usage all while keeping it lightweight and beginner-friendly.

Example: How to Monitor Website and API Uptime Using Azure Functions and .NET

Disclaimer: This example is purely for educational purposes. There are better ways to write code and applications that can optimize this example. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.

You can also find the full source code on GitHub: FunctionUptime Project

Prerequisites

Before starting, make sure you have the following installed:

  • .NET SDK: Download and install the .NET SDK if you haven’t already.
  • Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
  • C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.
  • Azure Functions Extension: Extension to manage Azure Functions directly from VS Code.
  • Azurite Extension: Extension to manage a local emulator for Azure Storage from VS Code
  • An Azure account: You can create one for free at https://azure.microsoft.com/free

Step 1 – Move Configuration to Application Settings

In the initial version of our uptime monitor, we hardcoded values like the website URL and the Logic App endpoint directly in the function. While this works, it’s not ideal.

If you want to make your Azure Function flexible, reusable, and production-ready, it’s best to move all configuration values outside of your code.

🎯 What We’ll Move to Settings:

  • The URL to monitor (urlToCheck)
  • The Logic App endpoint (logicAppUrl)
  • The schedule expression (TimerTrigger cron value)

Update local.settings.json (for local testing)

Your local.settings.json file should look like this:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "TimerSchedule": "0 0 */12 * * *",
    "UrlToCheck": "https://www.ottorinobruni.com",
    "LogicAppUrl": "https://your-logic-app-url"
  }
}

This way, you can easily test with different values without modifying the code.

How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2 – Settings

Update Your Function Code

Now we’ll change the code to read values from the environment.

[Function("CheckWebsiteUptime")]
public async Task RunAsync([TimerTrigger("%TimerSchedule%")] TimerInfo myTimer)
{
    var urlToCheck = Environment.GetEnvironmentVariable("UrlToCheck");
    var logicAppUrl = Environment.GetEnvironmentVariable("LogicAppUrl");
    var httpClient = new HttpClient();

    try
    {
        var response = await httpClient.GetAsync(urlToCheck);
        if (!response.IsSuccessStatusCode)
        {
            _logger.LogWarning($"Website is not reachable. Status code: {response.StatusCode}");
            await httpClient.PostAsync(logicAppUrl, null);
        }
        else
        {
            _logger.LogInformation($"Website responded successfully at {DateTime.Now}");
        }
    }
    catch (Exception ex)
    {
        _logger.LogError($"Error checking website: {ex.Message}");
        await httpClient.PostAsync(logicAppUrl, null);
    }
}

Benefits

  • You can reuse the function for different websites without touching the code
  • The schedule can be changed without redeploying
  • You’re following best practices for configuration management in cloud environments
How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2 – Environment Variables

Step 2 – Send Dynamic Content to the Logic App

In Part 1, our Azure Function sent a blank request to the Logic App whenever the website check failed. While this worked, it wasn’t very informative. The email had no context no URL, no error code, no timestamp.

Now we’ll make things smarter by sending a JSON payload with real information about what happened. This allows the Logic App to include dynamic content in the email, making the alert far more useful.

Update the Logic App Trigger Schema

In the “When an HTTP request is received” trigger:

  1. Click “Use sample payload to generate schema”
  2. Paste this JSON:
{
  "url": "https://www.ottorinobruni.com",
  "status": "fail",
  "statusCode": 500,
  "timestamp": "2025-06-06T12:00:00Z"
}

This tells the Logic App to expect this structure when the Function calls it.

How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2 – Request Body

Use Dynamic Values in Email

In the “Send an email (V2)” action:

Subject:

Alert for @{triggerBody()?['url']}

Body:

Status: @{triggerBody()?['status']}
HTTP Status Code: @{triggerBody()?['statusCode']}
Checked At: @{triggerBody()?['timestamp']}

This turns your email into a real-time report with context.

How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2 – Email Body

Step 3 – Update Azure Function to Send JSON

Update your Function code like this:

var payload = new
{
    url = urlToCheck,
    status = "fail",
    statusCode = (int)response?.StatusCode,
    timestamp = DateTime.UtcNow.ToString("o")
};

var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");

await _httpClient.PostAsync(logicAppUrl, content);

And in the catch block:

var errorPayload = new
{
    url = urlToCheck,
    status = "error",
    statusCode = 0,
    timestamp = DateTime.UtcNow.ToString("o")
};

var errorJson = JsonSerializer.Serialize(errorPayload);
var errorContent = new StringContent(errorJson, Encoding.UTF8, "application/json");

await _httpClient.PostAsync(logicAppUrl, errorContent);

Result

Now when your site is down or returns an error, you’ll receive a clear, informative email with:

  • The URL that failed
  • The reason (status code or exception)
  • The exact time of failure

Step 4 – Optimize HttpClient Usage

In the previous examples, we created a new HttpClient instance every time the function ran. While this works, it’s not a best practice — especially in serverless environments.

Repeatedly creating HttpClient instances can lead to:

  • Socket exhaustion
  • Unstable performance
  • Unnecessary memory usage

The recommended approach is to reuse a single HttpClient across function executions. Let’s fix that now.

Use a static readonly HttpClient

This is the easiest and most lightweight solution perfect for Azure Functions in isolated mode.

Update your function class like this:

public class CheckWebsiteUptime
{
    private readonly ILogger _logger;
    private static readonly HttpClient _httpClient = new HttpClient();

    public CheckWebsiteUptime(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<CheckWebsiteUptime>();
    }

    [Function("CheckWebsiteUptime")]
    public async Task RunAsync([TimerTrigger("%TimerSchedule%")] TimerInfo myTimer)
    {
        var urlToCheck = Environment.GetEnvironmentVariable("UrlToCheck");
        var logicAppUrl = Environment.GetEnvironmentVariable("LogicAppUrl");

        string status = "ok";
        int statusCode = 200;
        bool shouldNotify = false;

        try
        {
            var response = await _httpClient.GetAsync(urlToCheck);

            if (!response.IsSuccessStatusCode)
            {
                status = "fail";
                statusCode = (int)response.StatusCode;
                shouldNotify = true;
                _logger.LogWarning($"Website is not reachable. Status code: {statusCode}");
            }
            else
            {
                _logger.LogInformation($"Website responded successfully at {DateTime.Now}");
            }
        }
        catch (Exception ex)
        {
            status = "error";
            statusCode = 0;
            shouldNotify = true;
            _logger.LogError($"Error checking website: {ex.Message}");
        }

        if (shouldNotify)
        {
            var payload = new
            {
                url = urlToCheck,
                status = status,
                statusCode = statusCode,
                timestamp = DateTime.UtcNow.ToString("o")
            };

            var json = JsonSerializer.Serialize(payload);
            var content = new StringContent(json, Encoding.UTF8, "application/json");

            await _httpClient.PostAsync(logicAppUrl, content);
        }
    }
}

Why this works:

  • HttpClient is thread-safe and meant to be reused
  • Declaring it static readonly ensures it is initialized only once
  • No need for Dependency Injection or advanced configuration
How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2 – Payload

Can I Use Dependency Injection with AddHttpClient()?

Yes, it’s possible to register and inject HttpClient properly using the IServiceCollection with:

builder.Services.AddHttpClient();

This approach works well in more advanced Azure Function projects. However, it requires additional setup and adds a dependency on the DI container configuration.

For the sake of simplicity and to keep our project lightweight and beginner-friendly, we’re using a static readonly HttpClient — which is perfectly safe and recommended for small, focused functions like this one.

Testing the Dynamic Email Integration

To make sure everything was working correctly, I simulated a failure scenario by changing the monitored URL to:

https://www.ottorinobruni.test

This domain does not exist, so the HTTP request failed as expected. The Azure Function caught the error and triggered the Logic App with the dynamic JSON payload.

As a result, I received a well-formatted email with all the relevant information:

Status: error  
HTTP Status Code: 0  
Checked At: 2025-06-06T19:42:00Z  

This confirms that the Logic App correctly parses the payload and dynamically fills in the subject and body of the email, making the alert much more useful and informative.

How to Monitor Website and API Uptime Using Azure Functions and .NET – Part 2 – Uptime Alert

Conclusion

In this second part of our uptime monitoring project, we made our solution much smarter and more robust without adding complexity or cost.

We learned how to:

  • Move configuration values to local.settings.json for better flexibility
  • Send detailed JSON payloads to our Logic App with real-time status info
  • Customize the email alert with dynamic data
  • Reuse HttpClient safely using a simple static readonly pattern

What’s great is that all of this runs entirely on the Azure free tier, with:

  • Azure Functions triggering on schedule
  • Logic Apps sending emails dynamically
  • No need for external libraries or third-party services

It’s amazing how easy it is to build personal tools that solve real problems using serverless technology. With just a bit of C# and a free Azure account, we’ve created a solution that could monitor websites, APIs, or any service you care about reliably and automatically.

Curious how we built the base version of this monitor? Check out Part 1 of the series.

If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.

Thanks for reading!

🚀 Discover CodeSwissKnife, your all-in-one, offline toolkit for developers!

Click to explore CodeSwissKnife 👉

Leave A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.