We are experiencing significant delay during user login due to a slow response from the following API:
/api/abp/application-configuration?includeLocalizationResources=false
This API call is taking longer than expected and is impacting the overall login performance and user experience. Observed Behavior
- The API call consistently takes longer than expected.
- This results in delayed application loading after login.
- The issue is reproducible across multiple attempts.
Expected Behavior
- The API should respond within an acceptable time (ideally under 500 ms).
- The login and application initialization process should be smooth and fast.
Attachments
Please find the relevant screenshots below demonstrating the issue:
71 Answer(s)
-
0
/api/abp/application-configurationis expected to be called right after login (Angular/MVC UIs use it to bootstrap current user, settings, features, permissions, etc.). When this endpoint is slow, it’s almost always because one (or more) of the contributors behind it is doing expensive work (typically: permission/policy resolution, settings retrieval, or any customIApplicationConfigurationContributoryou added), or because the app is doing “first request” warm-up work (cold start, DB connection pool warmup, etc.).
1) Identify what’s slow (server-side) with minimal changes
Enable detailed timing on the server to see where time is spent for that single request:
Turn on ABP/Microsoft logs at least for the configuration pipeline categories in your AuthServer/Host:
Volo.Abp.AspNetCore.Mvc.ApplicationConfigurationsVolo.Abp.AuthorizationVolo.Abp.SettingsVolo.Abp.FeatureManagement(if enabled)Microsoft.EntityFrameworkCore.Database.Command(to catch slow SQL)
Reproduce login once and inspect the logs for:
- slow SQL queries
- repeated authorization checks
- remote calls (in microservice/gateway scenarios)
- any custom contributor executing heavy code
This step is the fastest way to get a definitive root cause because the endpoint itself is just an “aggregator”.
2) The most common causes + fixes
A) Granted policies (permissions) computation is slow
application-configurationreturnsauth.grantedPolicies. If you have:- a lot of permissions,
- many roles/claims,
- custom permission value providers hitting DB, this part can dominate response time.
Fix approaches:
- Reduce expensive DB work in custom
PermissionValueProviderimplementations. - Ensure related tables (user-role, role-permission, etc.) are properly indexed.
- Avoid per-permission database queries (N+1) in any customization.
B) Settings retrieval is slow (especially if stored in DB) If you heavily use Setting Management and many modules/settings are enabled, the endpoint reads a large settings set.
Fix approaches:
- Ensure distributed cache is configured and healthy (e.g., Redis in production).
- Verify there are no slow DB queries from setting stores.
C) Cold start / first request after deployment If the first login after an idle period is slow but subsequent ones are fast, it’s typically:
- app cold start (JIT, DI graph creation),
- first DB connection establishment,
- first-time cache population.
Fix approaches:
- Deploy with Release and production optimizations.
- Keep the app “warm” (health checks / periodic pings).
- Ensure DB connection pooling is correctly configured.
D) Reverse proxy / gateway adds latency In tiered or microservice setups,
application-configurationmay be proxied through gateways. This can add:- extra hop latency,
- token validation overhead,
- DNS/TLS overhead.
Fix approaches:
- Compare timing when calling the underlying service directly vs via gateway.
- Enable HTTP/2 between gateway and services if possible.
- Ensure token validation metadata is cached and not refetched frequently.
3) What you can do immediately (safe, ABP-aligned)
- Confirm whether it’s always ~4s or mostly the first request:
- If only first request is slow, focus on cold-start and caching.
- Check whether
auth.grantedPoliciesis huge:- In the response JSON, if
auth.grantedPoliciescontains many entries, permission resolution is a prime suspect.
- In the response JSON, if
- Ensure caching (especially distributed cache) is enabled/working in production.
4) Verified reference for this endpoint
- https://abp.io/docs/latest/framework/api-development/standard-apis/configuration
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
hi
Can you try to share full
/api/abp/application-configurationHTTP request debug logs?liming.ma@volosoft.com
https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
Have you used Redis? If so. Can you disable it and try again?
Thanks.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hello,
We have sent the full Administration service debug logs to your email (liming.ma@volosoft.com) from neel.m@technoheaven.net with the subject "Slow response time of application-configuration during login".
Please check the email for the attached logs.
Thanks, Neel Kotadia
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
I've analyzed your admin server logs. Your AdministrationService has 4,233 permissions registered from multiple microservices. This is expected in the microservice template with Dynamic Permission Store enabled.
From the logs:
- Cache hit: ~650ms (e.g., request at
05:48:03) - Cache miss: 10+ seconds (e.g., request at
05:44:45)
We suspect the bottleneck is the Redis cache operations with this large number of permissions. To confirm, could you temporarily disable Redis caching and use in-memory cache instead to test the same API call?
You can do this by removing the
AbpCachingStackExchangeRedisModuledependency and the related Redis configuration from your AdministrationService, then compare the response time.This will help us confirm the root cause before providing a solution.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Cache hit: ~650ms (e.g., request at
-
0
Hello, I have changed in a TMSMSAdministrationServiceModule.cs
but after this i am getting this issue.[08:40:35 WRN] The cookie 'XSRF-TOKEN' has set 'SameSite=None' and must also set 'Secure'. [08:40:35 DBG] Executing AbpApplicationConfigurationAppService.GetAsync()... [08:40:35 DBG] Executed AbpApplicationConfigurationAppService.GetAsync(). [08:40:35 INF] Executing ObjectResult, writing value of type 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationDto'. [08:40:35 INF] Executed action Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController.GetAsync (Volo.Abp.AspNetCore.Mvc) in 66.8065ms [08:40:35 INF] Executed endpoint 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController.GetAsync (Volo.Abp.AspNetCore.Mvc)' [08:40:35 INF] Request finished HTTP/1.1 GET http://adminserver/api/abp/application-configuration?includeLocalizationResources=false - 200 null application/json; charset=utf-8 100.208ms [08:40:35 INF] Request starting HTTP/1.1 GET http://adminserver/api/abp/application-localization?cultureName=en&onlyDynamics=false - null null [08:40:35 INF] CORS policy execution successful. [08:40:35 DBG] Get dynamic claims cache for user: 0e3c4557-57fd-31b4-7781-3a1df656b05c [08:40:35 DBG] Refresh dynamic claims for user: 0e3c4557-57fd-31b4-7781-3a1df656b05c from remote service. [08:40:35 INF] Start processing HTTP request POST https://auth.activitylinker.com/api/account/dynamic-claims/refresh [08:40:35 INF] Sending HTTP request POST https://auth.activitylinker.com/api/account/dynamic-claims/refresh [08:40:35 INF] Received HTTP response headers after 23.7656ms - 204 [08:40:35 INF] End processing HTTP request after 23.8603ms - 204 [08:40:35 WRN] Failed to refresh remote dynamic claims cache for user: 0e3c4557-57fd-31b4-7781-3a1df656b05c Volo.Abp.AbpException: Failed to refresh remote claims for user: 0e3c4557-57fd-31b4-7781-3a1df656b05c at Volo.Abp.Security.Claims.RemoteDynamicClaimsPrincipalContributorCacheBase
1.GetAsync(Guid userId, Nullable1 tenantId) at Volo.Abp.Security.Claims.RemoteDynamicClaimsPrincipalContributorBase2.ContributeAsync(AbpClaimsPrincipalContributorContext context) [08:40:35 INF] Executing endpoint 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationLocalizationController.GetAsync (Volo.Abp.AspNetCore.Mvc)' [08:40:35 INF] Route matched with {area = "abp", action = "Get", controller = "AbpApplicationLocalization", page = ""}. Executing controller action with signature System.Threading.Tasks.Task1[Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationDto] GetAsync(Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationRequestDto) on controller Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationLocalizationController (Volo.Abp.AspNetCore.Mvc). [08:40:35 INF] Executing ObjectResult, writing value of type 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationDto'. [08:40:35 INF] Executed action Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationLocalizationController.GetAsync (Volo.Abp.AspNetCore.Mvc) in 6.8293ms [08:40:35 INF] Executed endpoint 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationLocalizationController.GetAsync (Volo.Abp.AspNetCore.Mvc)' [08:40:35 INF] Request finished HTTP/1.1 GET http://adminserver/api/abp/application-localization?cultureName=en&onlyDynamics=false - 200 null application/json; charset=utf-8 34.1723ms [08:40:37 DBG] Executing HealthCheck collector HostedService. [08:40:37 INF] Start processing HTTP request GET http://[::]/health-status [08:40:37 INF] Sending HTTP request GET http://[::]/health-status [08:40:37 ERR] GetHealthReport threw an exception when trying to get report from /health-status configured with name AdministrationService Health Status. System.Net.Http.HttpRequestException: IPv4 address 0.0.0.0 and IPv6 address ::0 are unspecified addresses that cannot be used as a target address. (Parameter 'hostName') ([::]:80) ---> System.ArgumentException: IPv4 address 0.0.0.0 and IPv6 address ::0 are unspecified addresses that cannot be used as a target address. (Parameter 'hostName') at System.Net.Dns.GetHostEntryOrAddressesCoreAsync(String hostName, Boolean justReturnParsedIp, Boolean throwOnIIPAny, Boolean justAddresses, AddressFamily family, CancellationToken cancellationToken) at System.Net.Sockets.Socket.ConnectAsync(SocketAsyncEventArgs e, Boolean userSocket, Boolean saeaCancelable) at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ConnectAsync(Socket socket, Boolean saeaCancelable) at System.Net.Sockets.Socket.ConnectAsync(EndPoint remoteEP, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken) --- End of inner exception stack trace --- at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.InjectNewHttp11ConnectionAsync(QueueItem queueItem)Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
We have identified and resolved the issue.
Investigation: As per your suggestion, we temporarily removed AbpCachingStackExchangeRedisModule from Administration service to test with in-memory cache.
After that change, the Administration service started calling: POST https://auth.activitylinker.com/api/account/dynamic-claims/refresh on every request. AuthServer returned 204 OK but Administration service threw: "Failed to refresh remote dynamic claims cache for user"
This was causing a continuous redirect loop on every page load.
Fix Applied: We set WebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled = false in TMSMSAdministrationServiceModule.cs:
public override void PreConfigureServices(ServiceConfigurationContext context) { PreConfigure<WebRemoteDynamicClaimsPrincipalContributorOptions>(options => { options.IsEnabled = false; }); }
After this change redirect loop stopped and login works correctly.
We have also sent you an email at liming.ma@volosoft.com from neel.m@technoheaven.net with the logs captured after removing AbpCachingStackExchangeRedisModule. Please check your email for the attached log file.
Questions:
- Is this the correct permanent fix or just a workaround?
- Should we keep AbpCachingStackExchangeRedisModule removed or add it back?
- What is the proper way to enable remote dynamic claims in our microservice setup?
Thanks, Neel Kotadia Techno Heaven
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
ABP version is 9.2.0
please let me know that whatever code you will going to share with me that i need to add in my all micorservices including sass and identity and auth and auditlogging
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
We have identified the root cause and developed a proper fix that has been tested against real ABP module infrastructure. Here is everything you need.
Root cause
ABP's default
PermissionStorestores each permission result as a separate Redis Hash entry. With 4,233 permissions in your system, every request triggers thousands of individual Redis commands:- Warm cache: 4,233 individual Redis HMGET commands (~650ms)
- Cold cache: 4,233 HMGET + DB query + 4,233 HSET + 4,233 EXPIRE (~10+ seconds)
This is a scalability limitation in ABP's current permission caching design.
The fix
We override
PermissionStoreto store all granted permissions per provider as a single Redis entry. For example, instead of 4,233 separate entries for role "admin", it stores one entry:bulk_pn:R,pk:admin → ["Orders.View", "Orders.Create", ...]- Warm cache: 1 Redis GET
- Cold cache: 1 Redis GET + 1 DB query + 1 Redis SET
- Cache invalidation when permissions change: works correctly across all instances via Redis ✅
Step 1 — Revert your previous workarounds
In
TMSMSAdministrationServiceModule.cs:- Re-add
typeof(AbpCachingStackExchangeRedisModule)to your module dependencies if you removed it - Remove the
WebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled = falseline fromPreConfigureServices
Step 2 — Add these 3 files to your AdminService project
Replace
YourNamespacewith your actual project namespace.PermissionGrantBulkCacheItem.csusing System; using System.Collections.Generic; using System.Linq; namespace YourNamespace; [Serializable] public class PermissionGrantBulkCacheItem { public string[] GrantedPermissions { get; set; } public PermissionGrantBulkCacheItem() { GrantedPermissions = Array.Empty<string>(); } public PermissionGrantBulkCacheItem(IEnumerable<string> grantedPermissions) { GrantedPermissions = grantedPermissions.ToArray(); } public static string CalculateCacheKey(string providerName, string providerKey) { return $"bulk_pn:{providerName},pk:{providerKey}"; } }BulkPermissionStore.csusing System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Volo.Abp.Authorization.Permissions; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.PermissionManagement; namespace YourNamespace; [Dependency(ReplaceServices = true)] public class BulkPermissionStore : PermissionStore { protected IDistributedCache<PermissionGrantBulkCacheItem> BulkCache { get; } public BulkPermissionStore( IPermissionGrantRepository permissionGrantRepository, IDistributedCache<PermissionGrantCacheItem> cache, IDistributedCache<PermissionGrantBulkCacheItem> bulkCache, IPermissionDefinitionManager permissionDefinitionManager) : base(permissionGrantRepository, cache, permissionDefinitionManager) { BulkCache = bulkCache; } protected override async Task<PermissionGrantCacheItem> GetCacheItemAsync( string name, string providerName, string providerKey) { var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey); return new PermissionGrantCacheItem(bulkItem.GrantedPermissions.Contains(name)); } protected override async Task<List<KeyValuePair<string, PermissionGrantCacheItem>>> GetCacheItemsAsync( string[] names, string providerName, string providerKey) { var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey); var grantedSet = new HashSet<string>(bulkItem.GrantedPermissions); return names .Select(name => new KeyValuePair<string, PermissionGrantCacheItem>( CalculateCacheKey(name, providerName, providerKey), new PermissionGrantCacheItem(grantedSet.Contains(name)))) .ToList(); } protected virtual async Task<PermissionGrantBulkCacheItem> GetOrLoadBulkCacheItemAsync( string providerName, string providerKey) { var cacheKey = PermissionGrantBulkCacheItem.CalculateCacheKey(providerName, providerKey); var cacheItem = await BulkCache.GetAsync(cacheKey); if (cacheItem != null) { Logger.LogDebug("BulkPermissionStore: cache hit for {ProviderName}:{ProviderKey}.", providerName, providerKey); return cacheItem; } Logger.LogDebug("BulkPermissionStore: cache miss for {ProviderName}:{ProviderKey}, loading from DB.", providerName, providerKey); using (PermissionGrantRepository.DisableTracking()) { var grantedPermissions = await PermissionGrantRepository.GetListAsync(providerName, providerKey); cacheItem = new PermissionGrantBulkCacheItem(grantedPermissions.Select(p => p.Name)); } await BulkCache.SetAsync(cacheKey, cacheItem); Logger.LogDebug("BulkPermissionStore: cached {Count} granted permissions for {ProviderName}:{ProviderKey}.", cacheItem.GrantedPermissions.Length, providerName, providerKey); return cacheItem; } }PermissionGrantBulkCacheItemInvalidator.csusing System.Threading.Tasks; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.EventBus; using Volo.Abp.MultiTenancy; using Volo.Abp.PermissionManagement; namespace YourNamespace; public class PermissionGrantBulkCacheItemInvalidator : ILocalEventHandler<EntityChangedEventData<PermissionGrant>>, ITransientDependency { protected IDistributedCache<PermissionGrantBulkCacheItem> BulkCache { get; } protected ICurrentTenant CurrentTenant { get; } public PermissionGrantBulkCacheItemInvalidator( IDistributedCache<PermissionGrantBulkCacheItem> bulkCache, ICurrentTenant currentTenant) { BulkCache = bulkCache; CurrentTenant = currentTenant; } public virtual async Task HandleEventAsync(EntityChangedEventData<PermissionGrant> eventData) { var cacheKey = PermissionGrantBulkCacheItem.CalculateCacheKey( eventData.Entity.ProviderName, eventData.Entity.ProviderKey); using (CurrentTenant.Change(eventData.Entity.TenantId)) { await BulkCache.RemoveAsync(cacheKey, considerUow: true); } } }
Step 3 — No other changes needed
[Dependency(ReplaceServices = true)]onBulkPermissionStoretells ABP to automatically replace the defaultPermissionStore.PermissionGrantBulkCacheItemInvalidatoris auto-registered viaITransientDependency. No module file changes are required.After deployment, please check the logs for
BulkPermissionStore: cache hitandcache missmessages to confirm it is working.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hello TMSMS.AdministrationService/ └── Permissions/ ├── BulkPermissionStore.cs ├── PermissionGrantBulkCacheItem.cs └── PermissionGrantBulkCacheItemInvalidator.cs
is this fine ?
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hello. I did, and here's the log. ----------------------------------Before Login------------------------------ [11:06:11 INF] Notification is sent on same window time.
[11:06:11 DBG] HealthReportCollector - health report execution history saved.
[11:06:11 DBG] HealthReport history already exists and is in the same state, updating the values.
[11:06:11 DBG] HealthReportCollector has completed.
[11:06:11 DBG] HealthCheck collector HostedService executed successfully.
[11:06:11 DBG] BulkPermissionStore: cache miss for U:0e3c4557-57fd-31b4-7781-3a1df656b05c, loading from DB.
[11:06:11 DBG] BulkPermissionStore: cached 0 granted permissions for U:0e3c4557-57fd-31b4-7781-3a1df656b05c.
[11:06:11 DBG] BulkPermissionStore: cache miss for R:admin, loading from DB.
[11:06:12 DBG] BulkPermissionStore: cached 5798 granted permissions for R:admin.
[11:06:12 DBG] BulkPermissionStore: cache miss for C:Angular, loading from DB.
[11:06:12 DBG] BulkPermissionStore: cached 0 granted permissions for C:Angular.
------------------------------------ after login------------------------------
[11:08:13 WRN] The cookie 'XSRF-TOKEN' has set 'SameSite=None' and must also set 'Secure'.
[11:08:13 DBG] Executing AbpApplicationConfigurationAppService.GetAsync()...
[11:08:13 DBG] BulkPermissionStore: cache hit for U:0e3c4557-57fd-31b4-7781-3a1df656b05c.
[11:08:13 DBG] BulkPermissionStore: cache hit for R:admin.
[11:08:13 DBG] BulkPermissionStore: cache hit for C:Angular.
[11:08:13 DBG] Executed AbpApplicationConfigurationAppService.GetAsync().
[11:08:13 INF] Executing ObjectResult, writing value of type 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationDto'.
[11:08:13 INF] Executed action Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController.GetAsync (Volo.Abp.AspNetCore.Mvc) in 776.5724ms
-----------------------------------When i open one route via side bar in another tab-------------------------------------- ionController (Volo.Abp.AspNetCore.Mvc).
[11:09:30 WRN] The cookie 'XSRF-TOKEN' has set 'SameSite=None' and must also set 'Secure'.
[11:09:30 DBG] Executing AbpApplicationConfigurationAppService.GetAsync()...
[11:09:30 DBG] BulkPermissionStore: cache miss for U:0162719b-2c7b-adb3-a629-3a1dfef2a46e, loading from DB.
[11:09:30 DBG] BulkPermissionStore: cached 181 granted permissions for U:0162719b-2c7b-adb3-a629-3a1dfef2a46e.
[11:09:30 DBG] BulkPermissionStore: cache miss for R:admin, loading from DB.
[11:09:30 DBG] BulkPermissionStore: cached 4314 granted permissions for R:admin.
[11:09:30 DBG] BulkPermissionStore: cache hit for C:Angular.
[11:09:30 DBG] Executed AbpApplicationConfigurationAppService.GetAsync().
[11:09:30 INF] Executing ObjectResult, writing value of type 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationDto'.
[11:09:30 INF] Executed action Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController.GetAsync (Volo.Abp.AspNetCore.Mvc) in 576.4374ms
[11:09:30 INF] Executed endpoint 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController.GetAsync (Volo.Abp.AspNetCore.Mvc)'
[11:09:30 INF] Request finished HTTP/1.1 GET http://adminserver/api/abp/application-configuration?includeLocalizationResources=false - 200 null application/json; charset=utf-8 631.2903ms
[11:09:31 INF] Request starting HTTP/1.1 GET http://adminserver/api/abp/application-localization?cultureName=en&onlyDynamics=false - null null
I want to understand one thing: why, when I redirect any route in another tab, does it always take time? And I want to know one thing: how your cache system works, like user-wise or something else
And still this taking 1.5 or 2 sec. Why?
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
How the cache works:
It caches all granted permissions per provider in a single Redis entry:
U:{userId}— direct permissions for that specific userR:{roleName}— permissions for that role (shared across all users with the same role)C:{clientName}— permissions for that client
Why does opening a new tab always take time?
The first time any user/role is accessed, it's a cold cache — one DB query runs to load and cache all permissions. After that, it's a single Redis
GET. This is why the first request per user is a bit slower, but subsequent ones are instant.Why still 1.5–2 seconds?
/api/abp/application-configurationdoes a lot more than just permission checks — it also loads localization resources, feature grants, settings, auth info, and more. The permission part is now optimized (single RedisGET), but the remaining time comes from those other operations. This is expected behavior.From your logs, the permission-related work is now effectively instant on cache hits. The overall request time is now in the 600–800ms range, which is a significant improvement from the original 10+ seconds.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hello **Why does opening a new tab always take time?
The first time any user/role is accessed, it's a cold cache—one DB query runs to load and cache all permissions. After that, it's a single Redis GET. This is why the first request per user is a bit slower, but subsequent ones are instant. **
Suppose in this I have an open website in one tab. After I open any page in another tab via a redirecting click from the first tab of the side menu bar, will it always take time?
Regarding localization resources, we have another API available. Still, why does it takes 1.5 sec?
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Why does opening a new tab always take time?
Since this is an MVC application (not a SPA), every page navigation or new tab triggers a full page load. The browser re-runs all the initialization scripts, which call
application-configurationandapplication-localizationon every single page load — there's no in-memory state carried over between tabs. This is expected MVC behavior.The slowness might be largely network latency
Looking at your network panel, static assets like JS bundles and font files are also taking 500–700ms:
main.js(522 kB) → 982msruntime.js(25.5 kB) → 605ms- Font files → 562–704ms
These files require almost zero server-side processing, so the 500–700ms is essentially pure network round-trip time. This suggests the delay is largely from the distance between your users and the server, not from the application code itself.
Suggestion: test locally first
Run the app locally (client and server on the same machine) and check the API response times. If
application-localizationresponds in ~200–400ms locally, the remaining slowness in production is a network/infrastructure issue, not application code.Optimizing the localization API for MVC
The
application-localizationresponse contains static localization data that rarely changes. In an MVC app where every page load re-fetches it, you can tell the browser to cache it locally and skip the request on subsequent navigations.Add the following middleware in your module's
OnApplicationInitialization, beforeapp.UseConfiguredEndpoints():app.Use(async (ctx, next) => { if (ctx.Request.Path.StartsWithSegments("/api/abp/application-localization")) { ctx.Response.OnStarting(() => { ctx.Response.Headers.CacheControl = "public, max-age=600"; // cache for 10 minutes return Task.CompletedTask; }); } await next(); });After this change, the browser will cache the localization response for 10 minutes. Every page navigation within that window will skip the API call entirely — no network request, no server processing. This should eliminate the 1.43s from subsequent page loads.
Note: If your application is multi-tenant and different tenants have different dynamic localization texts (via the Language Management module), be cautious with this approach — the browser cache does not include tenant context. In that case, you may want to skip browser caching or reduce
max-ageto a smaller value.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
You only need to add it to your
adminserver— that's the only service the browser is calling for localization.That said, I'd actually recommend holding off on this change for now. Looking at your network panel again, static assets like JS files and fonts are also taking 500–700ms, which have no server-side processing at all. This tells us the main bottleneck is network latency between your users and the server, not the localization API itself. Adding browser caching for localization would help a little, but it won't solve the underlying latency issue.
I'd suggest testing the app locally first (client and server on the same machine). If everything feels fast locally, the slowness you're seeing in production is a network/infrastructure issue that needs to be addressed at that level.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Thanks for testing locally! The good news is that
application-localizationis now only 113ms locally, which confirms localization itself is not a problem — the 1.43s you saw earlier was mainly network latency.However,
application-configurationis still taking 1.45s locally, so let me ask a few questions:- Is the
BulkPermissionStoredeployed in this local environment? Can you check the logs forBulkPermissionStore: cache hitorBulkPermissionStore: cache missmessages? - Is this the first request after starting the service (cold cache), or did you refresh the page multiple times and it's still 1.45s every time?
- The response size is 224 kB — in the previous production test it was 20.4 kB. Are these different environments or different configurations?
Also, would it be possible for you to provide a simple reproduction project that demonstrates this issue? It doesn't need to include your full business logic — just the basic setup with the same number of permissions and the same service configuration. This would allow me to download it and debug locally to pinpoint exactly where the time is being spent.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Is the
-
0
Hello 1.Is the BulkPermissionStore deployed in this local environment? Can you check the logs for BulkPermissionStore: cache hit or BulkPermissionStore: cache miss messages? yes i have added on locally
2.Is this the first request after starting the service (cold cache), or did you refresh the page multiple times and it's still 1.45s every time?
No , i have refresh the page multiple times
Also, would it be possible for you to provide a simple reproduction project that demonstrates this issue? It doesn't need to include your full business logic — just the basic setup with the same number of permissions and the same service configuration. This would allow me to download it and debug locally to pinpoint exactly where the time is being spent.
in this what you wnat?
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Thanks for confirming. Before we go the reproduction project route, we found two more bottlenecks in the code and have optimizations ready for you to try.
We analyzed the code path that runs after the permission cache is loaded, and found:
The built-in
PermissionStore.IsGrantedAsync(string[])takes the cached results and callsFormattedStringValueExtracter.Extractto parse each permission name from the cache key — with 3 providers (User, Role, Client) × 4000+ permissions, that's ~12,000 string parsing operations per request, even when the cache is warm.The built-in
PermissionChecker.IsGrantedAsync(string[])looks up each permission definition individually (4000+ async calls) instead of loading them all at once.
Please make the following changes:
1. Replace your existing
BulkPermissionStore.cswith this updated version (addedIsGrantedAsyncoverride):using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Volo.Abp; using Volo.Abp.Authorization.Permissions; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.PermissionManagement; [Dependency(ReplaceServices = true)] public class BulkPermissionStore : PermissionStore { protected IDistributedCache<PermissionGrantBulkCacheItem> BulkCache { get; } public BulkPermissionStore( IPermissionGrantRepository permissionGrantRepository, IDistributedCache<PermissionGrantCacheItem> cache, IDistributedCache<PermissionGrantBulkCacheItem> bulkCache, IPermissionDefinitionManager permissionDefinitionManager) : base(permissionGrantRepository, cache, permissionDefinitionManager) { BulkCache = bulkCache; } protected override async Task<PermissionGrantCacheItem> GetCacheItemAsync( string name, string providerName, string providerKey) { var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey); return new PermissionGrantCacheItem(bulkItem.GrantedPermissions.Contains(name)); } protected override async Task<List<KeyValuePair<string, PermissionGrantCacheItem>>> GetCacheItemsAsync( string[] names, string providerName, string providerKey) { var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey); var grantedSet = new HashSet<string>(bulkItem.GrantedPermissions); return names .Select(name => new KeyValuePair<string, PermissionGrantCacheItem>( CalculateCacheKey(name, providerName, providerKey), new PermissionGrantCacheItem(grantedSet.Contains(name)))) .ToList(); } public override async Task<MultiplePermissionGrantResult> IsGrantedAsync( string[] names, string providerName, string providerKey) { Check.NotNullOrEmpty(names, nameof(names)); var result = new MultiplePermissionGrantResult(); var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey); var grantedSet = new HashSet<string>(bulkItem.GrantedPermissions); foreach (var name in names) { result.Result.Add(name, grantedSet.Contains(name) ? PermissionGrantResult.Granted : PermissionGrantResult.Undefined); } return result; } protected virtual async Task<PermissionGrantBulkCacheItem> GetOrLoadBulkCacheItemAsync( string providerName, string providerKey) { var cacheKey = PermissionGrantBulkCacheItem.CalculateCacheKey(providerName, providerKey); var cacheItem = await BulkCache.GetAsync(cacheKey); if (cacheItem != null) { Logger.LogDebug("BulkPermissionStore: cache hit for {ProviderName}:{ProviderKey}.", providerName, providerKey); return cacheItem; } Logger.LogDebug("BulkPermissionStore: cache miss for {ProviderName}:{ProviderKey}, loading from DB.", providerName, providerKey); using (PermissionGrantRepository.DisableTracking()) { var grantedPermissions = await PermissionGrantRepository.GetListAsync(providerName, providerKey); cacheItem = new PermissionGrantBulkCacheItem(grantedPermissions.Select(p => p.Name)); } await BulkCache.SetAsync(cacheKey, cacheItem); Logger.LogDebug("BulkPermissionStore: cached {Count} granted permissions for {ProviderName}:{ProviderKey}.", cacheItem.GrantedPermissions.Length, providerName, providerKey); return cacheItem; } }2. Add this new file
OptimizedPermissionChecker.cs:using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp; using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; using Volo.Abp.Security.Claims; using Volo.Abp.SimpleStateChecking; [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IPermissionChecker))] public class OptimizedPermissionChecker : IPermissionChecker, ITransientDependency { public ILogger<OptimizedPermissionChecker> Logger { get; set; } protected IPermissionDefinitionManager PermissionDefinitionManager { get; } protected ICurrentPrincipalAccessor PrincipalAccessor { get; } protected ICurrentTenant CurrentTenant { get; } protected IPermissionValueProviderManager PermissionValueProviderManager { get; } protected ISimpleStateCheckerManager<PermissionDefinition> StateCheckerManager { get; } public OptimizedPermissionChecker( ICurrentPrincipalAccessor principalAccessor, IPermissionDefinitionManager permissionDefinitionManager, ICurrentTenant currentTenant, IPermissionValueProviderManager permissionValueProviderManager, ISimpleStateCheckerManager<PermissionDefinition> stateCheckerManager) { PrincipalAccessor = principalAccessor; PermissionDefinitionManager = permissionDefinitionManager; CurrentTenant = currentTenant; PermissionValueProviderManager = permissionValueProviderManager; StateCheckerManager = stateCheckerManager; Logger = NullLogger<OptimizedPermissionChecker>.Instance; } public virtual async Task<bool> IsGrantedAsync(string name) { return await IsGrantedAsync(PrincipalAccessor.Principal, name); } public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string name) { Check.NotNull(name, nameof(name)); var permission = await PermissionDefinitionManager.GetOrNullAsync(name); if (permission == null) { return false; } if (!permission.IsEnabled) { return false; } if (!await StateCheckerManager.IsEnabledAsync(permission)) { return false; } var multiTenancySide = CurrentTenant.GetMultiTenancySide(); if (!permission.MultiTenancySide.HasFlag(multiTenancySide)) { return false; } var isGranted = false; var context = new PermissionValueCheckContext(permission, claimsPrincipal); foreach (var provider in PermissionValueProviderManager.ValueProviders) { if (context.Permission.Providers.Any() && !context.Permission.Providers.Contains(provider.Name)) { continue; } var result = await provider.CheckAsync(context); if (result == PermissionGrantResult.Granted) { isGranted = true; } else if (result == PermissionGrantResult.Prohibited) { return false; } } return isGranted; } public virtual async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names) { return await IsGrantedAsync(PrincipalAccessor.Principal, names); } public virtual async Task<MultiplePermissionGrantResult> IsGrantedAsync( ClaimsPrincipal? claimsPrincipal, string[] names) { Check.NotNull(names, nameof(names)); var result = new MultiplePermissionGrantResult(); if (!names.Any()) { return result; } var multiTenancySide = CurrentTenant.GetMultiTenancySide(); var allPermissions = (await PermissionDefinitionManager.GetPermissionsAsync()) .ToDictionary(p => p.Name); var permissionDefinitions = new List<PermissionDefinition>(); var permissionsNeedingStateCheck = new List<PermissionDefinition>(); foreach (var name in names) { if (!allPermissions.TryGetValue(name, out var permission)) { result.Result.Add(name, PermissionGrantResult.Prohibited); continue; } result.Result.Add(name, PermissionGrantResult.Undefined); if (permission.IsEnabled && permission.MultiTenancySide.HasFlag(multiTenancySide)) { if (permission.StateCheckers.Any()) { permissionsNeedingStateCheck.Add(permission); } else { permissionDefinitions.Add(permission); } } } if (permissionsNeedingStateCheck.Any()) { var stateCheckResult = await StateCheckerManager.IsEnabledAsync( permissionsNeedingStateCheck.ToArray()); foreach (var permission in permissionsNeedingStateCheck) { if (stateCheckResult[permission]) { permissionDefinitions.Add(permission); } } } foreach (var provider in PermissionValueProviderManager.ValueProviders) { var permissions = permissionDefinitions .Where(x => !x.Providers.Any() || x.Providers.Contains(provider.Name)) .ToList(); if (permissions.IsNullOrEmpty()) { continue; } var context = new PermissionValuesCheckContext(permissions, claimsPrincipal); var multipleResult = await provider.CheckAsync(context); foreach (var grantResult in multipleResult.Result.Where(x => result.Result.ContainsKey(x.Key))) { switch (grantResult.Value) { case PermissionGrantResult.Granted: { if (result.Result[grantResult.Key] != PermissionGrantResult.Prohibited) { result.Result[grantResult.Key] = PermissionGrantResult.Granted; } break; } case PermissionGrantResult.Prohibited: result.Result[grantResult.Key] = PermissionGrantResult.Prohibited; permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key); break; } } if (result.AllProhibited) { break; } } return result; } }The other two files (
PermissionGrantBulkCacheItem.csandPermissionGrantBulkCacheItemInvalidator.cs) remain unchanged. Cache invalidation still works the same way.Please try these changes first and share the
application-configurationresponse time. If the improvement is still not enough, we can proceed with the reproduction project approach.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)



