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. 🚀