
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.

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

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:
- Click “Use sample payload to generate schema”
- 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.

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.

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

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.

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 simplestatic 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 👉