Serverless with Azure Functions · Lesson 2 of 5
Triggers and Bindings — Timer, Queue, Blob, Service Bus
Trigger Types Overview
HTTP Trigger:
→ Invoked by an HTTP request
→ Use for: lightweight APIs, webhooks, FHIR callbacks
→ Cold start: first invocation after idle may be slow
Timer Trigger:
→ Invoked on a CRON schedule
→ Use for: nightly report generation, prescription expiry jobs, data sync
→ Does NOT fire missed runs by default — use WEBSITE_TIME_ZONE for local time
Service Bus Trigger:
→ Invoked when a message arrives on a queue or topic subscription
→ Use for: processing integration events, outbox relay, notification dispatch
→ At-least-once delivery — your handler must be idempotent
Blob Trigger:
→ Invoked when a blob is created or modified in a container
→ Use for: processing uploaded patient documents, PDF generation
→ Note: can lag up to 10 minutes for large containers — use Event Grid trigger for real-timeHTTP Trigger
// Isolated Worker model (.NET 8) — preferred over in-process
// NuGet: Microsoft.Azure.Functions.Worker, Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
public class PatientWebhookFunction
{
private readonly IMediator _mediator;
public PatientWebhookFunction(IMediator mediator) => _mediator = mediator;
[Function("PatientAdmissionWebhook")]
public async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "webhooks/patient-admission")]
HttpRequest req,
CancellationToken ct)
{
var body = await req.ReadFromJsonAsync<PatientAdmissionWebhookPayload>(ct);
if (body is null) return new BadRequestResult();
var command = new ProcessPatientAdmissionCommand(
body.PatientId, body.WardId, body.AdmittedAt);
var result = await _mediator.Send(command, ct);
return result.IsSuccess
? new OkResult()
: new BadRequestObjectResult(result.Error.Message);
}
}Timer Trigger — Scheduled Jobs
// Expire pending prescriptions that have been waiting more than 48 hours
public class ExpirePrescriptionsFunction
{
private readonly IPrescriptionRepository _repository;
private readonly IClock _clock;
public ExpirePrescriptionsFunction(
IPrescriptionRepository repository, IClock clock)
{
_repository = repository;
_clock = clock;
}
// CRON expression: "0 */6 * * *" = every 6 hours
// "0 0 2 * * *" = daily at 2:00 AM UTC
[Function("ExpirePrescriptions")]
public async Task RunAsync(
[TimerTrigger("0 */6 * * *")] TimerInfo timerInfo,
CancellationToken ct)
{
if (timerInfo.IsPastDue)
{
// The function missed a scheduled invocation (e.g., function was stopped)
// Decide: process anyway or skip
}
var cutoff = _clock.UtcNow.AddHours(-48);
var expiring = await _repository.GetPendingOlderThanAsync(cutoff, ct);
foreach (var prescription in expiring)
{
prescription.Expire();
await _repository.SaveAsync(prescription, ct);
}
}
}Service Bus Trigger — Event Processing
// Process PrescriptionCreated integration events from Service Bus
public class PrescriptionCreatedFunction
{
private readonly IPharmacyDispenseService _dispenseService;
public PrescriptionCreatedFunction(IPharmacyDispenseService dispenseService) =>
_dispenseService = dispenseService;
[Function("ProcessPrescriptionCreated")]
public async Task RunAsync(
[ServiceBusTrigger(
topicName: "prescriptioncreated-events",
subscriptionName: "pharmacy-service",
Connection: "ServiceBus")]
ServiceBusReceivedMessage message,
ServiceBusMessageActions messageActions,
CancellationToken ct)
{
var @event = message.Body.ToObjectFromJson<PrescriptionCreatedIntegrationEvent>();
try
{
await _dispenseService.CreateDispenseTaskAsync(@event.PrescriptionId, ct);
await messageActions.CompleteMessageAsync(message, ct);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// On failure: dead-letter instead of letting Service Bus retry
// Include reason for dead-lettering (aids investigation)
await messageActions.DeadLetterMessageAsync(
message,
deadLetterReason: "ProcessingFailed",
deadLetterErrorDescription: ex.Message,
ct);
}
}
}Blob Trigger — File Processing
// Process uploaded patient documents (e.g., scan and index content)
public class PatientDocumentProcessorFunction
{
private readonly IDocumentIndexingService _indexing;
public PatientDocumentProcessorFunction(IDocumentIndexingService indexing) =>
_indexing = indexing;
[Function("ProcessPatientDocument")]
public async Task RunAsync(
[BlobTrigger("patient-documents/{patientId}/{name}", Connection = "AzureStorage")]
Stream documentStream,
string patientId,
string name,
CancellationToken ct)
{
await _indexing.IndexDocumentAsync(
patientId: Guid.Parse(patientId),
documentName: name,
content: documentStream,
ct: ct);
}
}
// Note on Blob Trigger latency:
// If real-time processing is required (under 1 minute), use:
// → Event Grid trigger: notified immediately when blob is uploaded
// Blob trigger polled by Azure Functions every ~10 minutes for large containersProgram.cs for Isolated Worker
// Azure Functions Isolated Worker host setup
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureAppConfiguration(config =>
{
config.AddEnvironmentVariables();
// Add Key Vault in production:
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("KeyVaultUri")))
config.AddAzureKeyVault(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")!),
new DefaultAzureCredential());
})
.ConfigureServices((context, services) =>
{
var config = context.Configuration;
services.AddDbContext<PrescriptionsDbContext>(opts =>
opts.UseSqlServer(config.GetConnectionString("Clinical")));
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(ProcessPatientAdmissionCommand).Assembly));
services.AddSingleton<IClock, SystemClock>();
services.AddScoped<IPrescriptionRepository, PrescriptionRepository>();
})
.Build();
await host.RunAsync();Production issue I've seen: A Timer Trigger function ran a nightly data sync at 2:00 AM. The function was deployed in a Consumption plan. Due to idle timeout, the function was deallocated at 1:58 AM — just before its scheduled run. Azure Functions detected the "missed" execution 10 minutes later (when the next polling cycle ran), but
timerInfo.IsPastDuewas true. The function code checkedIsPastDueand skipped the run ("we'll catch it at 2:00 AM tomorrow"). The data sync didn't run for 3 consecutive nights before a monitoring alert caught it. Fix: use a Premium or Dedicated plan (no cold start/idle timeout) for Timer Triggers with strict scheduling requirements, and handleIsPastDue = trueby running the job, not skipping it.
Key Takeaway
Azure Functions triggers route invocations to your code: HTTP (request-driven), Timer (scheduled), Service Bus (event-driven), Blob (file-driven). Use the Isolated Worker model (.NET 8) — not the legacy in-process model. For Service Bus triggers, complete messages manually with
ServiceBusMessageActionsand dead-letter on unrecoverable failures. HandletimerInfo.IsPastDue = true— decide explicitly whether to run or skip missed executions. Use Key Vault + Managed Identity for all secrets in function apps.