April 27, 2026

How to Fix "App Unresponsive" in Power Apps Using Chunked Processing

Introduction

You've built a Power App that works perfectly on small datasets. Then a colleague opens it with 400 real product records - and the dreaded "App Unresponsive" warning appears. The app isn't broken. It's doing exactly what you asked - processing everything simultaneously, on a single thread, with no capacity left for anything else.

Power Apps applications frequently handle large datasets with complex, record-level business logic such as pricing rules, tax computation, and discounts. When this logic runs entirely on the client side in a single operation, performance problems like freezing and "App Unresponsive" warnings become inevitable.

In this post, we walk through exactly why this happens and demonstrate how chunking - processing records in smaller sequential batches - resolves the problem without changing your underlying business logic.

Technical Challenge

Consider a typical Power Apps scenario where each record may contain nested or related data, complex per-record calculations such as pricing rules, tax computation, or discounts, and where all processing is performed using client-side Power Fx formulas.

When a large dataset is processed in a single operation, the application may freeze and users may receive "Unresponsive" warnings - even though the app is still executing logic in the background.

Important: Performance degradation depends not only on the number of records, but also on the complexity of calculations per record and the user's system configuration - CPU, memory, browser, and device performance.

Why Power Apps Become Unresponsive

1. All Records Are Processed at Once

A single ForAll() loop executes calculations for every record simultaneously. This becomes especially impactful when working with large collections that contain heavy logic per record. For light calculations, even 1,000+ records may process without issue. For heavy nested formulas, as few as 200–300 records can trigger an unresponsive state.

2. High Memory and CPU Consumption

Each record evaluation produces intermediate calculation results. When hundreds of records are processed simultaneously, memory and CPU usage spike - overwhelming the client device. Three compounding failures occur at once:

  • Memory Spike - All intermediate results are held in memory simultaneously, causing excessive consumption that overwhelms the client device.
  • CPU Overload - Parallel processing of hundreds of complex calculations saturates the processor, leaving no capacity for UI rendering or user interaction.
  • UI Freeze - The render thread is blocked entirely, preventing any screen updates until all processing completes.

3. UI Thread Blocking

Power Apps evaluates formulas on the same thread that renders the UI. While large calculations run, the UI cannot refresh - making the app appear frozen even if execution is still ongoing in the background.

Before Chunking: Processing All Records at Once

The following example processes all records in a single pass. While straightforward to write, this pattern blocks the UI thread for the entire duration and is the primary cause of "App Unresponsive" warnings in production.

Power Apps processing all records at once
ForAll(
    colProducts,
    Patch(
        colProducts,
        ThisRecord,
        {
            BaseRevenue: BasePrice * QuantitySold,
            DiscountAmount: BasePrice * QuantitySold * DiscountRate,
            TaxAmount: ((BasePrice * QuantitySold) -
                (BasePrice * QuantitySold * DiscountRate)) * TaxRate,
            FinalRevenue:
                (BasePrice * QuantitySold)
                - (BasePrice * QuantitySold * DiscountRate)
                - (((BasePrice * QuantitySold) -
                   (BasePrice * QuantitySold * DiscountRate)) * TaxRate)
        }
    )
);

Why this causes problems: The threshold at which this fails is not fixed. For light calculations, even 1,000+ records may process without issue. For heavy calculations - like the nested formula above - as few as 200–300 records can trigger an unresponsive state. The risk scales directly with per-record logic complexity.

After Chunking: Processing Records in Batches

The chunked implementation below processes records incrementally. The business logic is identical - only the delivery mechanism changes. Start with a batch size of 100 and adjust based on the complexity of your formulas and the capabilities of your target devices.

// Adjust _chunkSize based on formula complexity and device capability
Set(_chunkSize, 100);
Set(_totalRecords, CountRows(colProducts));

ForAll(
    Sequence(RoundUp(_totalRecords / _chunkSize, 0)) As _batch,
    With(
        {
            _start: (_batch.Value - 1) * _chunkSize + 1,
            _end:   Min(_batch.Value * _chunkSize, _totalRecords)
        },
        ForAll(
            Sequence(_end - _start + 1, _start, 1) As _row,
            With(
                { _p: Last(FirstN(colProducts, _row.Value)) },
                Patch(
                    colProducts,
                    _p,
                    {
                        BaseRevenue:    _p.BasePrice * _p.QuantitySold,
                        DiscountAmount: _p.BasePrice * _p.QuantitySold * _p.DiscountRate,
                        TaxAmount:      ((_p.BasePrice * _p.QuantitySold) -
                                         (_p.BasePrice * _p.QuantitySold * _p.DiscountRate)) * _p.TaxRate,
                        FinalRevenue:   (_p.BasePrice * _p.QuantitySold)
                                        - (_p.BasePrice * _p.QuantitySold * _p.DiscountRate)
                                        - (((_p.BasePrice * _p.QuantitySold) -
                                            (_p.BasePrice * _p.QuantitySold * _p.DiscountRate)) * _p.TaxRate)
                    }
                )
            )
        )
    )
);

How it works: The outer ForAll(Sequence(...)) iterates over batch numbers. For each batch, a With() scope calculates the start and end record indices. The inner loop retrieves and processes each record individually using Last(FirstN()) - a standard Power Fx idiom for positional record access. The UI thread is free to refresh between batches, keeping the experience smooth throughout.

Frequently asked questions

Does chunking change the final output or results?

No. Chunking only changes the order in which records are processed, not the calculations applied to each one. The final state of your collection will be identical to what a single ForAll() would produce - it simply gets there without freezing the app.

When should I not use chunking?

For small collections - typically fewer than 100 records with simple arithmetic - a standard ForAll() loop remains perfectly appropriate and easier to maintain. Chunking adds structural complexity, so apply it where the performance benefit is real.

How do I know if my batch size is too large?

If users still report freezes or unresponsive warnings after applying chunking, reduce the batch size. Start at 100, test on your lowest-spec target device, and decrease by 25–50 until the experience is consistently smooth.

Can this pattern be used with SharePoint lists instead of local collections?

Yes, with some adaptation. When working directly against a SharePoint data source, the same batching logic applies - however, each Patch() call will be a network request, so consider the additional latency and delegate where possible to reduce client-side load.

Conclusion

A single ForAll() loop across a large collection blocks the UI thread entirely until every record is processed - causing the freezes and "App Unresponsive" errors that users experience as crashes. The underlying logic isn't wrong; the delivery mechanism is.

Chunking resolves this by processing records in sequential batches. Memory stays bounded, the UI thread has room to breathe, and the app remains interactive throughout the operation - regardless of how complex your per-record formulas are.

How to Lock Down SharePoint Access with Sites.Selected Permissions

Introduction

If you've ever granted an app Sites.ReadWrite.All permissions in SharePoint, you know that sinking feeling you've just given it the keys to every single site in your tenant. For most applications, that's like using a sledgehammer to hang a picture frame.

There's a better way: Sites.Selected permissions. This feature lets you grant your application access to only the specific SharePoint sites it actually needs nothing more, nothing less. It's the principle of least privilege in action, and it's surprisingly straightforward to set up.

In this guide, I'll walk you through the complete process of configuring Sites.Selected with read-only access to a specific SharePoint site. We'll use Microsoft Graph Explorer (no PowerShell required), and by the end, you'll have a secure, properly scoped application that can only access the data it needs.

What Is Sites.Selected and Why Should You Care?

Sites.Selected is a Microsoft Graph permission that flips the traditional access model on its head. Instead of granting tenant-wide access upfront, you start with zero access and explicitly grant permissions to individual sites as needed.

Think of it this way:

  • Sites.ReadWrite.All = Master key to every door in the building
  • Sites.Selected = Specific key cards for only the rooms you need

This approach drastically reduces your attack surface. If your application's credentials are ever compromised, the blast radius is limited to just the sites you've explicitly granted access to not your entire SharePoint environment.

The Three-Phase Process

Setting up Sites.Selected permissions involves three distinct phases, each building on the last:

Phase Action Where
1 Create app registration & add Sites.Selected permission Azure Portal
2 Get SharePoint site ID Microsoft Graph Explorer
3 Grant read permission to site Microsoft Graph Explorer

Here's the crucial thing to understand: after Phase 1, your app has Sites.Selected consent but zero actual access. It's not until Phase 3 that you grant permission to specific sites. This two-step process is what makes the permission model secure.

What You'll Need

Before we dive in, make sure you have:

  • Global Admin or Application Admin role in Azure AD
  • SharePoint Admin access
  • The URL of the SharePoint site you want to grant access to

That's it. No PowerShell modules to install, no scripts to run just your browser and admin credentials.

Phase 1: Creating Your App Registration

The first phase happens entirely in the Azure Portal. We'll create a new app registration and add the Sites.Selected permission but remember, this doesn't grant access to anything yet.

Navigate to App Registrations

Head to https://portal.azure.com and sign in with your admin account. Use the search bar at the top to find App registrations and select it.

Register Your Application

Click + New registration and fill in these details:

Field Value
Name SharePoint-Reader-App
Supported account types Accounts in this organizational directory only
Redirect URI Leave blank

Click Register and you'll land on the app overview page.

Save Your Application ID

On the overview page, you'll see an Application (client) ID. This is a GUID that looks something like a3f44e63-e46e-4c31-9213-888c172ca160. Copy this and save it somewhere safe you'll need it in Phase 3.

Add the Sites.Selected Permission

In the left menu, click API permissions, then + Add a permission. Here's where it gets specific:

  1. Select Microsoft Graph
  2. Choose Application permissions (not Delegated this is critical)
  3. Search for Sites.Selected
  4. Check the box and click Add permissions

Application permissions are for background services and daemon apps that run without a signed-in user. Delegated permissions are for apps that act on behalf of a user. For Sites.Selected to work, you must use Application permissions.

Grant Admin Consent

Back on the API permissions page, click Grant admin consent for [Your Organization] and confirm. You'll see a green checkmark appear next to Sites.Selected with a status of Granted.

Important: At this point, your app has Sites.Selected consent, but it still can't access any sites. That's by design. Phase 3 is where you grant the actual site-level permissions.

Phase 2: Getting the SharePoint Site ID

To grant permissions to a specific site, you need its Site ID. This is a long, comma-separated string that uniquely identifies the site in your tenant. We'll use Microsoft Graph Explorer to retrieve it.

Open Graph Explorer and Sign In

Navigate to https://developer.microsoft.com/en-us/graph/graph-explorer and click Sign in to Graph Explorer in the top right. Use your admin account.

Consent to Permissions (Critical Step)

Here's something that trips people up: Graph Explorer needs its own permissions to make API calls. These are separate from your app's permissions.

Click the Modify permissions tab below the query box. Find these two permissions and consent to both:

  • Sites.Read.All (needed to retrieve the site ID)
  • Sites.FullControl.All (needed in Phase 3 to grant permissions)

For each permission, click Consent, then Accept in the pop-up. Verify both show a Consented status before proceeding.

Construct Your Query

Set the method dropdown to GET. Now you need to build the URL. The format is:

https://graph.microsoft.com/v1.0/sites/{tenant}.sharepoint.com:/sites/{sitename}

Let's say your site is https://contoso.sharepoint.com/sites/ProjectAlpha. Your Graph API URL would be:

https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/ProjectAlpha

Note: Use the site name from the URL, not the display name. If your site is called "Project Alpha Site" but the URL is ProjectAlpha, use ProjectAlpha.

Run the Query and Extract the Site ID

Click Run query. In the Response preview, you'll see JSON that looks like this:

{
  "id": "contoso.sharepoint.com,a1b2c3d4-1234-5678-abcd-111122223333,e5f6g7h8-4321-8765-dcba-444455556666",
  "name": "ProjectAlpha",
  "displayName": "Project Alpha",
  "webUrl": "https://contoso.sharepoint.com/sites/ProjectAlpha"
}

Copy the entire id value the whole thing, including the commas. This is your Site ID. Save it alongside your Application ID.

The Site ID format is {hostname},{site-collection-id},{web-id}. Don't try to reconstruct it manually or use just part of it you need the complete string.

Phase 3: Granting Read Permission to Your Site

This is where everything comes together. We'll use a POST request in Graph Explorer to grant your application read access to the specific site.

Set Up the POST Request

In Graph Explorer, change the method to POST. The URL format is:

https://graph.microsoft.com/v1.0/sites/{siteId}/permissions

Replace {siteId} with the full Site ID you copied in Phase 2. For our example:

https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,a1b2c3d4-1234-5678-abcd-111122223333,e5f6g7h8-4321-8765-dcba-444455556666/permissions

Add the Request Body

Click the Request body tab and enter this JSON:

{
  "roles": ["read"],
  "grantedToIdentities": [
    {
      "application": {
        "id": "YOUR-APPLICATION-CLIENT-ID",
        "displayName": "SharePoint-Reader-App"
      }
    }
  ]
}

Critical: Replace YOUR-APPLICATION-CLIENT-ID with the Application ID you saved in Phase 1. Using our example ID, the complete JSON would be:

{
  "roles": ["read"],
  "grantedToIdentities": [
    {
      "application": {
        "id": "a3f44e63-e46e-4c31-9213-888c172ca160",
        "displayName": "SharePoint-Reader-App"
      }
    }
  ]
}

The roles array specifies the permission level. We're using read for read-only access. Other options include write, manage, and fullcontrol.

Execute and Verify

Click Run query. If everything is configured correctly, you'll receive a 201 Created response with JSON that looks like:

{
  "id": "aTowaS50fG1zLnNwLmV4dHxlYTVmMDVlZ...",
  "roles": ["read"],
  "grantedToIdentitiesV2": [...],
  "grantedToIdentities": [...]
}

That id field in the response is the permission ID. Save it if you think you might need to update or revoke this permission later.

Verifying Everything Works

To confirm the permission was granted successfully, you can query the permissions endpoint. Change the method back to GET and use:

https://graph.microsoft.com/v1.0/sites/{siteId}/permissions

Click Run query. You should see your application listed in the response with roles set to ["read"].

Understanding Available Permission Roles

We used read in this guide, but Sites.Selected supports four permission levels:

Role Description
read Read-only access (used in this guide)
write Read and write access
manage Manage lists and libraries
fullcontrol Full control over the site

Choose the most restrictive role that meets your application's needs. If you only need to read documents, stick with read.

Common Issues and How to Fix Them

Here are the most common problems people run into and their solutions:

403 Forbidden Error

If you get a 403 error when trying to grant permissions in Phase 3, you likely haven't consented to Sites.FullControl.All in Graph Explorer. Go back to the Modify permissions tab and consent to it.

Site Not Found

Double-check that you're using the site name from the URL, not the display name. If your site URL is /sites/proj-alpha but the display name is Project Alpha Team Site, use proj-alpha.

Insufficient Privileges

Make sure you've clicked Modify permissions in Graph Explorer and consented to both Sites.Read.All and Sites.FullControl.All. These are Graph Explorer's permissions, not your app's.

Invalid Request Body

Check your JSON syntax carefully. Common mistakes include missing commas, mismatched brackets, or forgetting to replace YOUR-APPLICATION-CLIENT-ID with your actual Application ID.

Wrapping Up

Sites.Selected permissions represent a massive improvement in security posture for SharePoint integrations. Instead of granting blanket access to your entire tenant, you can scope applications down to exactly what they need and nothing more.

The process involves three phases: creating an app registration with Sites.Selected consent in Azure Portal, retrieving the Site ID using Graph Explorer, and granting site-specific permissions via a POST request. Each phase builds on the last, and by the end, you have a properly scoped application that follows the principle of least privilege.

If you're building SharePoint integrations, this should be your default approach.

If you have any questions you can reach out our SharePoint Consulting team here.

Deploying .NET Core Web API to IIS on Windows Server (Fix HTTP 500.19 Error)

Introduction

Deploying a .NET Core Web API to IIS on Windows Server is a standard requirement in enterprise environments. However, many developers encounter deployment failures such as HTTP Error 500.19 (Error Code: 0x8007000d) immediately after publishing.

This error is rarely complex. It is almost always caused by missing prerequisites, incorrect IIS configuration, or an improperly installed Hosting Bundle.

This guide walks you through the complete IIS deployment process step-by-step - and explains exactly how to diagnose and resolve HTTP 500.19 errors with confidence.

Prerequisites

Before beginning deployment, ensure the following components are installed and ready:

  • Windows Server installed and accessible
  • IIS (Internet Information Services) installed and running
  • .NET Core / .NET Hosting Bundle installed (version must match your project)
  • Visual Studio project built successfully in Release mode
  • Published output folder generated and accessible

Important: The Hosting Bundle version must precisely match your target framework (.NET 6, 7, 8, etc.). A version mismatch is one of the most common root causes of HTTP Error 500.19.

01 Install IIS

Open Server ManagerAdd Roles and Features → Select Role-based installation → Choose your server → Select Web Server (IIS).

During feature selection, ensure the following are included:

  • Web Server (IIS)
  • Application Development Features
  • .NET Extensibility
  • ASP.NET Core Module (if available)
  • Management Tools
  • IIS Management Console

Command - Verify IIS Manager

inetmgr

Run this command in the Run dialog (Win + R) to confirm IIS Manager opens successfully.

02 Install .NET Core Hosting Bundle

Download the Hosting Bundle from the official Microsoft .NET download page and install the version that precisely matches your project's target framework. Running a mismatched version is a leading cause of 500.19 errors and should be verified before any other troubleshooting.

Command - Restart IIS After Installation

iisreset

After installation, always restart IIS to ensure the .NET Core Module is properly registered.

03 Select Publish Target

  • Right-click your project in Solution Explorer → Click Publish
  • Select Folder as the publish target → Click Next
  • Provide a publish path, e.g.: C:\Users\YourName\Desktop\PublishedFolder
  • Click Finish to save the publish profile

04 Publish the Web API

  • Set Configuration to Release
  • Set Deployment Mode to Framework-dependent
  • Confirm Target Framework matches your project version
  • Enable Delete all existing files prior to publish to avoid stale artifacts
  • Click Publish and wait for the process to complete

Once published, copy the output folder to the IIS web root directory:

C:\inetpub\wwwroot\MyWebApi

05 Create Application Pool

  • Open IIS Manager → Click Application Pools
  • Click Add Application Pool in the Actions panel
  • Name: MyApiPool
  • .NET CLR Version: No Managed Code (required for all .NET Core apps)
  • Managed Pipeline Mode: Integrated

Setting the .NET CLR Version to No Managed Code is critical. .NET Core manages its own runtime independently and does not rely on the IIS CLR. Selecting a CLR version here will cause application pool startup errors.

06 Create Website in IIS

  • In IIS Manager, right-click Sites → Click Add Website
  • Site Name: MyWebApi
  • Physical Path: point to your published output folder
  • Application Pool: select MyApiPool (created in Step 05)
  • Binding Port: 80 (or a custom port such as 5000)
  • Click OK to create the site

07 Configure Windows Firewall (For Custom Ports)

If your IIS website is configured to use a custom port (e.g., 5000), you must create an inbound firewall rule to allow traffic on that port. Without this step, external requests will be blocked silently by Windows Firewall.

  • Press Win + R, type wf.msc, and press Enter
  • Click Inbound RulesNew Rule
  • Select Port as the rule type → Click Next
  • Choose TCP and enter 5000 under Specific local ports
  • Select Allow the connection → Click Next
  • Apply to profiles: Domain, Private, and Public
  • Name the rule MyWebApi Port 5000 → Click Finish

After the rule is created, verify connectivity by navigating to:

http://your-server-ip:5000

Common Issue: HTTP Error 500.19 - Internal Server Error

Error Code: 0x8007000d  |  HTTP Status: 500.19 – Internal Server Error

This error consistently points to one of the following root causes:

  • Hosting Bundle not installed - or the version does not match the target framework
  • Corrupted or invalid web.config - IIS cannot parse the configuration file
  • Missing AspNetCoreModuleV2 - module not registered after Hosting Bundle installation
  • Incorrect Application Pool configuration - CLR version not set to No Managed Code

Diagnosis Tip: After each corrective action, run iisreset from an elevated command prompt and re-test. Most 500.19 errors resolve after reinstalling the correct Hosting Bundle version followed by an IIS restart.

Conclusion

The majority of HTTP 500.19 errors are environment configuration issues - not application code problems. By correctly installing IIS, precisely matching the Hosting Bundle version to your target framework, setting the Application Pool to No Managed Code, and opening the required ports in Windows Firewall, you can deploy your .NET Core Web API reliably and predictably on any Windows Server environment.

Follow these steps in sequence, validate each stage before proceeding to the next, and run iisreset after any configuration change to ensure changes take effect immediately.