Introduction
Every Power Apps project I have rescued has the same origin story: someone built a beautiful prototype that worked perfectly for 3 users and 50 records. Then it went to production with 500 users and 50,000 records. Then everything broke.
This article is the catalog of mistakes I see repeatedly — across industries, across skill levels, across organizations. Some are technical. Some are architectural. Some are organizational. All of them are preventable if you know what to watch for.
I have organized them into five categories: Data Mistakes, Performance Mistakes, Architecture Mistakes, Security Mistakes, and Organizational Mistakes.
Category 1: Data Mistakes (Pitfalls 1–6)
Pitfall 1: Ignoring Delegation Limits
The mistake: Using non-delegable functions like CountRows, Search, SortByColumns with text comparisons, or Lookup with in-memory filtering on large data sources.
Why it hurts: The app silently returns incomplete data. No error. No warning. The user sees 500 records and believes that is all there is. Financial reports are wrong. Queries miss records. Compliance reporting has gaps.
// ❌ PITFALL: Non-delegable — returns max 500/2000 records
Filter(Expenses, Month(SubmittedDate) = 3)
// Month() is NOT delegable to Dataverse or SQL
// ✅ FIX: Use delegable date comparison
Filter(
Expenses,
SubmittedDate >= Date(2026, 3, 1) &&
SubmittedDate < Date(2026, 4, 1)
)
// Date comparisons ARE delegable
The rule: If you see a blue dotted underline in the formula bar, you have a delegation problem. Fix it before anything else.
Pitfall 2: Using SharePoint Lists as a Primary Database
The mistake: Storing 50,000+ records in a SharePoint list because "it was free and easy."
Why it hurts: SharePoint list view threshold is 5,000 items. Performance degrades dramatically. Complex filtering fails. No referential integrity. No transactions. No stored procedures. Lookup columns create N+1 query patterns.
{
"sharepoint_list_limits": {
"max_items_view_threshold": 5000,
"max_columns_per_list": 512,
"max_lookup_columns": 12,
"max_file_attachment_size_mb": 250,
"max_items_per_list_supported": 30000000,
"performance_cliff": "Noticeable at 2000+ items with complex views",
"when_acceptable": [
"Document libraries (SharePoint's strength)",
"Simple lists under 2000 items",
"Lists with flat structure (no complex lookups)",
"When budget genuinely cannot cover Dataverse"
],
"when_to_migrate": [
"More than 5000 items expected within 12 months",
"Need relational data (foreign keys, joins)",
"Need row-level security",
"Need calculated rollups or aggregations",
"Need offline/mobile scenarios"
]
}
}
Pitfall 3: Not Planning for Data Volume Growth
The mistake: Building a query that works with 100 records and assuming it will work with 100,000.
// ❌ PITFALL: Loads ALL records into memory
ClearCollect(colAllOrders, Orders);
Set(varTotalRevenue, Sum(colAllOrders, Amount));
// At 100 records: 0.5 seconds. At 100,000 records: never finishes.
// ✅ FIX: Server-side aggregation via Power Automate
Set(varIsCalculating, true);
'CalculateRevenue'.Run(
{
startDate: varStartDate,
endDate: varEndDate,
department: varDepartment
}
);
Set(varTotalRevenue, Value(varFlowResult.totalRevenue));
Set(varIsCalculating, false);
Pitfall 4: Storing Sensitive Data in Local Collections
The mistake: Caching user data, salaries, medical records, or authentication tokens in client-side collections.
// ❌ PITFALL: Sensitive data cached on client
ClearCollect(colEmployeeSalaries,
ShowColumns(Employees, "Name", "Salary", "SSN", "BankAccount")
);
// This data is now in browser memory, inspectable via dev tools
// ✅ FIX: Only retrieve what the current user should see
ClearCollect(colMyTeamSalaries,
Filter(
ShowColumns(Employees, "Name", "Salary"),
Manager.Email = User().Email
)
);
// Row-level security + column restriction + delegable
Pitfall 5: Not Indexing Dataverse Columns
The mistake: Filtering, sorting, or searching on columns that are not indexed. Every query becomes a full table scan.
Architecture Overview: # Check which columns are indexed in a Dataverse table
Pitfall 6: Using Patch Instead of SubmitForm
The mistake: Using Patch() for form submissions instead of the built-in SubmitForm() function.
// ❌ PITFALL: Manual Patch for forms
Patch(
Expenses,
Defaults(Expenses),
{
Title: txtTitle.Text,
Amount: Value(txtAmount.Text),
Category: drpCategory.Selected.Value,
Date: dpDate.SelectedDate,
Description: txtDescription.Text,
Merchant: txtMerchant.Text,
Status: "Submitted"
}
);
// Problems: No validation, no error handling, no form reset
// ✅ FIX: Use EditForm + SubmitForm
SubmitForm(frmExpense);
// Benefits: Built-in validation, OnSuccess/OnFailure handlers,
// automatic form reset, undo support, required field checking
Category 2: Performance Mistakes (Pitfalls 7–12)
Pitfall 7: Loading Everything on App.OnStart
The mistake: Putting every ClearCollect, Set, and API call in App.OnStart, creating a 15-second splash screen.
// ❌ PITFALL: App.OnStart does everything
// App.OnStart
ClearCollect(colEmployees, Employees);
ClearCollect(colDepartments, Departments);
ClearCollect(colProjects, Projects);
ClearCollect(colTimesheets, Timesheets);
ClearCollect(colExpenses, Expenses);
ClearCollect(colApprovals, Approvals);
Set(varUser, LookUp(Employees, Email = User().Email));
Set(varManager, LookUp(Employees, ID = varUser.ManagerID));
// 12 seconds to load. User stares at blank screen.
// ✅ FIX: Load only what the first screen needs
// App.OnStart (fast — 1-2 seconds)
Concurrent(
Set(varUser, LookUp(Employees, Email = User().Email)),
ClearCollect(colLookups, Departments)
);
// Screen.OnVisible (load screen-specific data on demand)
// scrDashboard.OnVisible
If(IsEmpty(colMyTimesheets),
ClearCollect(colMyTimesheets,
Filter(Timesheets, Employee.ID = varUser.ID &&
WeekStart >= DateAdd(Today(), -14, TimeUnit.Days))
)
);
Pitfall 8: Gallery Inside Gallery (N+1 Problem)
The mistake: Nesting a gallery inside another gallery, causing each parent row to execute its own query.
// ❌ PITFALL: Nested gallery = N+1 queries
// Parent gallery: galDepartments (Items = Departments) — 20 rows
// Child gallery: galEmployees (Items = Filter(Employees, DeptID = ThisItem.ID))
// Result: 1 query for departments + 20 queries for employees = 21 queries
// ✅ FIX: Pre-load and filter locally
// Screen.OnVisible
ClearCollect(colAllEmployees,
Filter(Employees, Status = "Active")
);
// Child gallery uses local collection (no additional queries)
// galEmployees.Items = Filter(colAllEmployees, DeptID = ThisItem.ID)
// Result: 1 query for departments + 1 query for employees = 2 queries
Pitfall 9: Images Without Optimization
The mistake: Users uploading 5MB photos from their iPhone directly into Dataverse image columns. Galleries with 50 items load 250MB of images.
Pitfall 10: Timer Polling Instead of Event-Driven Refresh
The mistake: Adding a Timer control that runs every 5 seconds to check for new data, consuming API calls and battery.
Pitfall 11: Complex Formulas in Gallery Templates
The mistake: Putting LookUp(), CountRows(), or Filter() calls in gallery item labels. Each row recalculates on every render.
// ❌ PITFALL: LookUp in every gallery row
// Gallery item label formula:
LookUp(Departments, ID = ThisItem.DepartmentID, Name)
// 50 gallery items = 50 LookUp calls on every render
// ✅ FIX: Create enriched collection once
ClearCollect(
colEnrichedEmployees,
AddColumns(
Filter(Employees, Status = "Active"),
"DepartmentName", LookUp(colDepartments, ID = DepartmentID, Name)
)
);
// Gallery uses colEnrichedEmployees — no per-row lookups
Pitfall 12: Not Using Concurrent() for Independent Loads
// ❌ PITFALL: Sequential loading (9 seconds total)
ClearCollect(colA, SourceA); // 3 seconds
ClearCollect(colB, SourceB); // 3 seconds
ClearCollect(colC, SourceC); // 3 seconds
// ✅ FIX: Parallel loading (3 seconds total)
Concurrent(
ClearCollect(colA, SourceA),
ClearCollect(colB, SourceB),
ClearCollect(colC, SourceC)
);
Category 3: Architecture Mistakes (Pitfalls 13–17)
Pitfall 13: One Mega-App Instead of Micro-Apps
The mistake: Building a single app with 40+ screens that handles expenses, timesheets, approvals, reporting, and admin functions.
{
"mega_app_symptoms": [
"More than 15 screens",
"Load time exceeds 10 seconds",
"Multiple unrelated data sources",
"Different user roles see completely different screens",
"App size exceeds 5MB (unpacked .msapp)"
],
"micro_app_architecture": {
"app_1": "Expense Submission (5 screens, 3s load)",
"app_2": "Expense Approval (3 screens, 2s load)",
"app_3": "Expense Reporting (4 screens, 3s load)",
"app_4": "Admin Configuration (3 screens, 2s load)",
"shared": "Component Library (reusable headers, nav, themes)"
},
"how_to_link": "Use Launch() for cross-app navigation with parameters"
}
Pitfall 14: Hardcoded Configuration Values
The mistake: Hardcoding environment URLs, API keys, email addresses, threshold values, and feature flags directly in formulas.
// ❌ PITFALL: Hardcoded everything
Set(varAPIEndpoint, "https://prod-api.contoso.com/v2");
Set(varApprovalThreshold, 5000);
Set(varAdminEmail, "john.smith@contoso.com");
// ✅ FIX: Configuration table in Dataverse
ClearCollect(
colConfig,
Filter(AppConfiguration,
AppName = "ExpenseTracker" &&
Environment = LookUp(EnvironmentSettings, IsCurrent, Name)
)
);
Set(varAPIEndpoint, LookUp(colConfig, Key = "APIEndpoint", Value));
Set(varApprovalThreshold, Value(LookUp(colConfig, Key = "ApprovalThreshold", Value)));
Set(varAdminEmail, LookUp(colConfig, Key = "AdminEmail", Value));
// Change config without republishing the app
Pitfall 15: No Error Handling Strategy
The mistake: Assuming every API call, Patch, and flow invocation will succeed. When one fails, the user sees a cryptic error or — worse — nothing at all.
// ❌ PITFALL: No error handling
Patch(Expenses, Defaults(Expenses), {Title: "New Expense"});
// If Patch fails: no feedback, no retry, data lost
// ✅ FIX: Comprehensive error handling
Set(varIsSaving, true);
IfError(
Patch(Expenses, Defaults(Expenses), {
Title: txtTitle.Text,
Amount: Value(txtAmount.Text)
}),
// Error handler
Notify(
"Unable to save expense. Please check your connection and try again. " &
"Error: " & FirstError.Message,
NotificationType.Error,
5000
);
Set(varSaveError, true);
// Log error for support team
'LogAppError'.Run(
User().Email,
"ExpenseCreate",
FirstError.Message,
Text(Now(), "yyyy-mm-dd hh:mm:ss")
),
// Success handler
Notify("Expense saved successfully", NotificationType.Success);
Set(varSaveError, false);
Navigate(scrDashboard, ScreenTransition.None)
);
Set(varIsSaving, false);
Pitfall 16: Building Without a Component Library
The mistake: Copy-pasting header bars, navigation menus, and styling across 10 apps. When the brand color changes, you update 10 apps manually.
Pitfall 17: No ALM (Application Lifecycle Management) Pipeline
The mistake: Developing directly in the production environment. Testing with live data. Publishing changes without backup.
# Minimum viable ALM pipeline
# 1. Export solution from Dev
pac solution export `
--path "solutions/ExpenseTracker_1.0.0.zip" `
--name "ExpenseTracker" `
--managed
# 2. Import to Test environment
pac solution import `
--path "solutions/ExpenseTracker_1.0.0.zip" `
--environment "https://test-org.crm.dynamics.com"
# 3. Run automated tests (Power Apps Test Engine)
pac test run `
--test-plan-file "tests/expense-smoke-tests.yaml"
# 4. If tests pass, import to Production
pac solution import `
--path "solutions/ExpenseTracker_1.0.0.zip" `
--environment "https://prod-org.crm.dynamics.com" `
--import-as-holding
Category 4: Security Mistakes (Pitfalls 18–21)
Pitfall 18: Relying on UI Hiding for Security
The mistake: Using Visible = false to hide admin buttons, assuming users cannot access admin functions. Anyone can inspect the app or share the admin screen deep link.
// ❌ PITFALL: Security by obscurity
// Admin button visible only to "admins"
btnAdmin.Visible = User().Email in ["admin@contoso.com"]
// Problems:
// - Anyone can deep-link to the admin screen
// - Data is still accessible via the data source
// - If someone gets added to the "admins" list by mistake, game over
// ✅ FIX: Server-side security (Dataverse security roles)
// 1. Create "Expense Admin" security role in Dataverse
// 2. Assign role to admin users
// 3. Configure table-level permissions (CRUD per role)
// 4. Configure column-level security for sensitive fields
// 5. UI visibility is cosmetic — server enforces access
Pitfall 19: Shared Connections with Elevated Permissions
The mistake: Using a shared connection with admin privileges so "the app works for everyone." Every user inherits admin-level database access.
Pitfall 20: No Audit Trail for Critical Actions
The mistake: Approvals, deletions, and financial transactions happen with no record of who did what, when, or why.
// ✅ Create audit log for every critical action
Patch(
AuditLog,
Defaults(AuditLog),
{
Action: "ExpenseApproved",
EntityType: "Expense",
EntityID: Text(varSelectedExpense.ID),
PerformedBy: User().Email,
PerformedAt: Now(),
OldValue: JSON(varSelectedExpense, JSONFormat.IndentFour),
NewValue: JSON({Status: "Approved", ApprovedAmount: varApprovedAmount}),
IPAddress: "Captured via flow",
Notes: txtApprovalNotes.Text
}
);
Pitfall 21: Exposing API Keys in Power Automate Expressions
The mistake: Putting API keys directly in HTTP action URLs or headers where they appear in flow run history.
Category 5: Organizational Mistakes (Pitfalls 22–25)
Pitfall 22: No Executive Sponsor
The mistake: Building Power Apps as a grass-roots initiative without executive air cover. The first time the project needs budget, headcount, or cross-department cooperation, it stalls.
Pitfall 23: Not Measuring Adoption
The mistake: Launching the app and assuming success because nobody complained. Meanwhile, 80% of users went back to their Excel spreadsheet.
{
"adoption_metrics_to_track": [
{
"metric": "Daily Active Users (DAU)",
"target": "50% of licensed users within 30 days",
"how": "Power Platform analytics + custom telemetry"
},
{
"metric": "Task Completion Rate",
"target": "85% of started tasks completed without abandonment",
"how": "Custom logging: track screen navigations and form submissions"
},
{
"metric": "Time to Complete Key Task",
"target": "Below 3 minutes for core workflow",
"how": "Timestamp on form open vs. form submit"
},
{
"metric": "Support Tickets Per Week",
"target": "Decreasing trend after launch",
"how": "Tag tickets with app name in helpdesk system"
},
{
"metric": "User Satisfaction (CSAT)",
"target": "4.0+ out of 5.0",
"how": "Monthly 3-question survey embedded in app"
}
]
}
Pitfall 24: Licensing Surprises
The mistake: Building an app with premium connectors, deploying to 500 users, then discovering the licensing cost three days before go-live.
| Scenario | License Required | Cost Impact |
|---|---|---|
| Canvas app + SharePoint only | Microsoft 365 (included) | $0 additional |
| Canvas app + Dataverse | Power Apps per user ($20/mo) or per app ($5/mo) | $12,000–$120,000/yr for 500 users |
| Model-driven app (any) | Power Apps per user ($20/mo) | $120,000/yr for 500 users |
| App + premium connector (SAP, Oracle, custom) | Power Apps per user ($20/mo) | $120,000/yr for 500 users |
| App + AI Builder | Power Apps per user + AI Builder capacity | $120,000+ add-on credits |
| Power Automate premium flow triggered by app | Per user ($15/mo) or per flow ($100/mo) | Varies significantly |
The rule: Check licensing requirements during Week 1 of the project. Not Week 12.
Pitfall 25: Building Without User Research
The mistake: IT decides what the app should do. IT builds the app. IT shows it to users. Users say "that is not how we work." IT blames users for not adopting.
{
"user_research_minimum_viable": {
"before_building": [
"Shadow 3 users doing the current process for 30 minutes each",
"Count the clicks/steps in their current workflow",
"Ask: What is the most annoying part of your current process?",
"Ask: If you could change one thing, what would it be?"
],
"during_building": [
"Show clickable prototype to 5 users after Week 1",
"Watch them try to complete a task. Do not help.",
"If 3 out of 5 cannot complete the task, redesign",
"Test every screen on mobile (most users will use mobile)"
],
"after_launch": [
"Monitor DAU daily for 30 days",
"Call 3 heavy users and 3 non-users after Week 2",
"Ask non-users: Why are you not using the app?",
"The answer is always one of: did not know it existed, too slow, does not match my workflow, my manager does not use it"
]
}
}
The Pitfall Severity Matrix
| # | Pitfall | Severity | Detection | Fix Effort |
|---|---|---|---|---|
| 1 | Delegation limits | Critical | Blue underline in editor | Medium |
| 2 | SharePoint as database | High | Performance complaints | High (migration) |
| 3 | No volume planning | High | Gradual performance decay | Medium |
| 4 | Sensitive data in collections | Critical | Security audit | Low |
| 5 | Missing indexes | Medium | Slow queries | Low |
| 6 | Patch instead of SubmitForm | Low | No validation errors | Low |
| 7 | Everything in OnStart | High | Slow app launch | Medium |
| 8 | Gallery N+1 | High | Network tab analysis | Medium |
| 9 | Unoptimized images | Medium | Gallery scroll lag | Low |
| 10 | Timer polling | Medium | API throttling | Low |
| 11 | Formulas in gallery items | Medium | Gallery scroll lag | Medium |
| 12 | No Concurrent() | Medium | Sequential slow loads | Low |
| 13 | Mega-app | High | 40+ screens, 10s+ load | High (refactor) |
| 14 | Hardcoded config | Medium | Env promotion failures | Low |
| 15 | No error handling | High | Silent failures | Medium |
| 16 | No component library | Medium | Inconsistent UX | Medium |
| 17 | No ALM pipeline | Critical | Prod incidents | High |
| 18 | UI hiding for security | Critical | Security audit | Medium |
| 19 | Shared elevated connections | Critical | Security audit | Medium |
| 20 | No audit trail | High | Compliance audit | Medium |
| 21 | Exposed API keys | Critical | Flow run history review | Low |
| 22 | No executive sponsor | High | Budget/staffing blocks | Organizational |
| 23 | Not measuring adoption | Medium | Unknown usage | Low |
| 24 | Licensing surprises | Critical | Budget review | Planning |
| 25 | No user research | High | Low adoption | Medium |
Architecture Decision and Tradeoffs
When designing low-code development solutions with Power Apps, consider these key architectural trade-offs:
| Approach | Best For | Tradeoff |
|---|---|---|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |
Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.
Validation and Versioning
- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.
Security and Governance Considerations
- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.
Cost and Performance Notes
- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.
Official Microsoft References
- https://learn.microsoft.com/power-apps/
- https://learn.microsoft.com/power-platform/admin/
- https://learn.microsoft.com/power-platform/guidance/
Public Examples from Official Sources
- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/power-apps/
- Sample repositories: https://github.com/microsoft/PowerApps-Samples
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.
Key Takeaways
- Delegation is the #1 technical trap — the blue dotted underline in the formula bar is your early warning system, never ignore it
- SharePoint lists are not databases — migrate to Dataverse when you expect 5,000+ records or need relational data
- Load data on demand, not all at once — use
Concurrent()and screen-levelOnVisibleinstead of bloatedApp.OnStart - Security through UI hiding is not security — always enforce access at the data layer with Dataverse security roles and column-level security
- Check licensing costs in Week 1, not Week 12 — premium connectors and Dataverse can add $120K+/year for 500 users
- Build micro-apps (5–8 screens) linked via
Launch(), not mega-apps with 40 screens and 15-second load times - Error handling is mandatory — wrap every
Patch(), API call, and flow invocation inIfError()with user-friendly messages - Measure adoption with DAU, task completion rate, and CSAT — "nobody complained" is not a success metric
- Shadow real users for 30 minutes before building — the workflow you imagine is never the workflow they actually follow
- Every pitfall on this list was made by smart, experienced people — the difference is whether you learn from their mistakes or repeat them
Discussion