Skip to main content

Background Process Execution

Last Updated: January 9, 2026

Learn how to run long-running bulk processes in the background without blocking the API request, and understand the implications for your BPM implementation.


The Problem

When processing large collections (e.g., 2000 items), the workflow can take several minutes to complete. If you start the process synchronously:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Issues:

  • ❌ HTTP request timeout (30-120 seconds)
  • ❌ Browser shows loading spinner indefinitely
  • ❌ User can't navigate away or do other work
  • ❌ If connection drops, process state is unclear
  • ❌ Poor user experience

The Solution: Fire-and-Forget Pattern

Start the workflow in the background and return immediately with a tracking ID:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Implementation Approach

Current BPM Implementation Status

Your current StartProcessCommand handler likely looks like this:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

The Issue: _bpmnEngine.ExecuteProcess() runs synchronously through all tasks until completion or a wait state.


Architecture Change

Modify the execution flow to:

  1. Create the process instance (status = "Pending")
  2. Return immediately with process instance ID
  3. Queue the process for background execution
  4. Background worker picks up and executes the process

Implementation

Step 1: Modify StartProcessCommand Handler

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Step 2: Add Background Job Queue Service

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Step 3: Add Background Worker Service

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Step 4: Register Services in Program.cs

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


API Controller Pattern

Controller Endpoint

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Frontend Integration

Starting the Process

async function startBulkProcess(items) {
try {
const response = await fetch('/api/bulk-process', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: items,
batchSize: 100
})
});

if (response.status === 202) { // Accepted
const result = await response.json();

// Show success message
showNotification('Bulk processing started', 'success');

// Start polling for status
pollProcessStatus(result.processInstanceId);
}
} catch (error) {
showNotification('Failed to start process: ' + error.message, 'error');
}
}

Polling for Status

function pollProcessStatus(processInstanceId) {
const intervalId = setInterval(async () => {
try {
const response = await fetch(`/api/bulk-process/${processInstanceId}/status`);
const status = await response.json();

// Update UI
updateProgressBar(status.percentComplete);
updateStats({
processed: status.processedItems,
total: status.totalItems,
success: status.successCount,
failed: status.failureCount
});

// Check if complete
if (status.isComplete) {
clearInterval(intervalId);

if (status.status === 'completed') {
showNotification('Bulk processing completed!', 'success');
loadResults(processInstanceId);
} else if (status.status === 'failed') {
showNotification('Bulk processing failed', 'error');
}
}
} catch (error) {
console.error('Error polling status:', error);
}
}, 3000); // Poll every 3 seconds
}

Progress UI Component

<div class="bulk-process-container">
<div class="progress-section">
<h3>Bulk Processing in Progress</h3>

<!-- Progress Bar -->
<div class="progress-bar">
<div id="progress-fill" class="progress-fill" style="width: 0%">
<span id="progress-text">0%</span>
</div>
</div>

<!-- Stats -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-label">Total Items</span>
<span id="total-items" class="stat-value">-</span>
</div>
<div class="stat-card">
<span class="stat-label">Processed</span>
<span id="processed-items" class="stat-value">-</span>
</div>
<div class="stat-card success">
<span class="stat-label">Success</span>
<span id="success-count" class="stat-value">-</span>
</div>
<div class="stat-card failure">
<span class="stat-label">Failed</span>
<span id="failure-count" class="stat-value">-</span>
</div>
</div>

<!-- Action Buttons -->
<div class="actions">
<button onclick="navigateAway()" class="btn btn-secondary">
Continue Working
</button>
<button onclick="viewResults()" class="btn btn-primary" disabled id="view-results-btn">
View Results
</button>
</div>
</div>
</div>

<script>
function updateProgressBar(percent) {
document.getElementById('progress-fill').style.width = percent + '%';
document.getElementById('progress-text').innerText = percent.toFixed(1) + '%';
}

function updateStats(stats) {
document.getElementById('total-items').innerText = stats.total;
document.getElementById('processed-items').innerText = stats.processed;
document.getElementById('success-count').innerText = stats.success;
document.getElementById('failure-count').innerText = stats.failed;

// Enable results button when complete
if (stats.processed === stats.total) {
document.getElementById('view-results-btn').disabled = false;
}
}

function navigateAway() {
// User can navigate away - process continues in background
localStorage.setItem('activeProcessId', currentProcessInstanceId);
window.location.href = '/dashboard';
}

// On page load, check for active processes
window.addEventListener('load', () => {
const activeProcessId = localStorage.getItem('activeProcessId');
if (activeProcessId) {
// Resume polling for this process
pollProcessStatus(activeProcessId);
}
});
</script>

Impact on Current BPM Implementation

1. Process Instance State Management

Current State: Process instances likely don't track execution state granularly

Required Changes:

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

2. Database Schema Changes

-- Add new columns to ProcessInstance table
ALTER TABLE ProcessInstance
ADD StartedAt DATETIME2 NULL,
CompletedAt DATETIME2 NULL,
ErrorMessage NVARCHAR(MAX) NULL,
CreatedBy NVARCHAR(255) NULL;

-- Add index for status queries
CREATE INDEX IX_ProcessInstance_Status_CreatedAt
ON ProcessInstance(Status, CreatedAt DESC);

3. BPMN Engine Modifications

No Breaking Changes Required!

The BPMN execution engine (BpmnExecutionEngine) doesn't need changes. It continues to execute processes synchronously, but now:

  • ✅ Executes in background thread (not API request thread)
  • ✅ Can take minutes/hours without timing out
  • ✅ API returns immediately with process ID
  • ✅ Clients poll for status updates

4. Command Handler Pattern

Before (Synchronous):

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

After (Asynchronous):

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

5. Scalability Considerations

Current Limitations:

  • Single instance can handle ~10-20 concurrent background processes
  • Each process blocks a thread during execution

Future Enhancements (if needed):

  • Multiple Workers: Scale background worker instances
  • Distributed Queue: Use RabbitMQ/Redis instead of in-memory Channel
  • Process Pooling: Limit concurrent executions to prevent overload
Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Alternative: SignalR for Real-Time Updates

Instead of polling, use SignalR for real-time push notifications:

SignalR Hub

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Update Progress from Worker

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Frontend SignalR Client

const connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/process-progress")
.build();

connection.on("ProgressUpdate", (update) => {
// Update UI in real-time
updateProgressBar(update.percentComplete);
updateStats(update);
});

connection.start().then(() => {
connection.invoke("SubscribeToProcess", processInstanceId);
});

Benefits:

  • âš¡ Real-time updates (no polling delay)
  • 🔋 Lower server load (no constant polling)
  • 📱 Better mobile experience

Migration Strategy

Phase 1: Add Background Queue (Non-Breaking)

  1. Add IBackgroundJobQueue interface and implementation
  2. Add ProcessExecutionBackgroundService hosted service
  3. Register services in DI container
  4. Deploy - no impact on existing functionality

Phase 2: Update StartProcessCommand Handler

  1. Modify handler to queue work instead of executing directly
  2. Add status tracking columns to database
  3. Deploy - existing synchronous callers still work (process just happens in background)

Phase 3: Update API Controllers

  1. Change POST endpoints to return 202 Accepted instead of 200 OK
  2. Add status polling endpoints
  3. Update frontend to poll for status
  4. Deploy with backward compatibility

Phase 4: Add Real-Time Updates (Optional)

  1. Add SignalR hub
  2. Update background worker to push updates
  3. Update frontend to use SignalR
  4. Deploy

Best Practices

✅ DO

  • Return 202 Accepted for async operations (not 200 OK)
  • Provide status URL in response for tracking
  • Implement timeout limits for background processes
  • Log extensively in background workers
  • Handle cancellation properly (CancellationToken)
  • Limit queue size to prevent memory issues
  • Add retry logic for transient failures
  • Send notifications when long processes complete

❌ DON'T

  • Return 200 OK for operations that aren't complete
  • Block HTTP requests for long-running operations
  • Forget to update process status on failure
  • Ignore CancellationToken in background work
  • Allow unlimited queue growth
  • Skip error handling in background workers

Testing Background Execution

Unit Test

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Integration Test

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


Conclusion

Impact Summary

AspectCurrentAfter Background Execution
API Response TimeMinutes (timeout)< 1 second ✅
User ExperienceBlocked/waitingCan navigate away ✅
ScalabilityLimitedBetter (controlled queue) ✅
Error HandlingUnclear stateTracked status ✅
Progress TrackingNonePolling/SignalR ✅

Required Changes

  1. ✅ Add background job queue - New service, no breaking changes
  2. ✅ Update StartProcessCommand handler - Queue work instead of executing
  3. ✅ Add status tracking - New DB columns, no schema breaking changes
  4. ✅ Update API controllers - Return 202 Accepted with status URL
  5. ✅ Add polling endpoints - New endpoints for status checking
  6. âš¡ Optional: Add SignalR - Real-time updates instead of polling

Minimal Implementation (1-2 hours)

If you want the quickest solution:

  1. Add IBackgroundJobQueue and BackgroundJobQueue classes
  2. Add ProcessExecutionBackgroundService hosted service
  3. Register in Program.cs
  4. Modify StartProcessCommandHandler to queue work
  5. Add /status endpoint to check process state

That's it! Your bulk processes now run in the background without blocking API requests. 🚀