Open Closed

Slow response time for /api/abp/application-configuration during login #10565


User avatar
0
vkelwan created

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:

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

71 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • /api/abp/application-configuration is 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 custom IApplicationConfigurationContributor you 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:

    1. Turn on ABP/Microsoft logs at least for the configuration pipeline categories in your AuthServer/Host:

      • Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
      • Volo.Abp.Authorization
      • Volo.Abp.Settings
      • Volo.Abp.FeatureManagement (if enabled)
      • Microsoft.EntityFrameworkCore.Database.Command (to catch slow SQL)
    2. 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-configuration returns auth.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 PermissionValueProvider implementations.
    • 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-configuration may 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)

    1. Confirm whether it’s always ~4s or mostly the first request:
      • If only first request is slow, focus on cold-start and caching.
    2. Check whether auth.grantedPolicies is huge:
      • In the response JSON, if auth.grantedPolicies contains many entries, permission resolution is a prime suspect.
    3. 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)
  • User Avatar
    0
    vkelwan created

    But on every time on page load also this call takes 2-3 secs

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Can you try to share full /api/abp/application-configuration HTTP 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)
  • User Avatar
    0
    vkelwan created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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 AbpCachingStackExchangeRedisModule dependency 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)
  • User Avatar
    0
    vkelwan created

    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.RemoteDynamicClaimsPrincipalContributorCacheBase1.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)
  • User Avatar
    0
    vkelwan created

    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:

    1. Is this the correct permanent fix or just a workaround?
    2. Should we keep AbpCachingStackExchangeRedisModule removed or add it back?
    3. 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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    What is your abp package version?

    I will share some code with you. Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    vkelwan created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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 PermissionStore stores 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 PermissionStore to 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:

    1. Re-add typeof(AbpCachingStackExchangeRedisModule) to your module dependencies if you removed it
    2. Remove the WebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled = false line from PreConfigureServices

    Step 2 — Add these 3 files to your AdminService project

    Replace YourNamespace with your actual project namespace.

    PermissionGrantBulkCacheItem.cs

    using 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.cs

    using 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.cs

    using 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)] on BulkPermissionStore tells ABP to automatically replace the default PermissionStore. PermissionGrantBulkCacheItemInvalidator is auto-registered via ITransientDependency. No module file changes are required.

    After deployment, please check the logs for BulkPermissionStore: cache hit and cache miss messages 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)
  • User Avatar
    0
    vkelwan created

    Hello can we have this root level this all 3 files ?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please try to add new files to the TMSMS.AdministrationService project.

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    vkelwan created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    No problem.

    Please build and run it, then check the logs.

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    vkelwan created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    How the cache works:

    It caches all granted permissions per provider in a single Redis entry:

    • U:{userId} — direct permissions for that specific user
    • R:{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-configuration does 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 Redis GET), 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)
  • User Avatar
    0
    vkelwan created

    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?

    And can we optimize this same localization resource, API?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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-configuration and application-localization on 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) → 982ms
    • runtime.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-localization responds 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-localization response 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, before app.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-age to a smaller value.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    vkelwan created

    Hello Add the following middleware in your module's OnApplicationInitialization, before app.UseConfiguredEndpoints(): this i need to add in my every microservice ?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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)
  • User Avatar
    0
    vkelwan created

    Hello Locally Also it's taking same time

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Thanks for testing locally! The good news is that application-localization is now only 113ms locally, which confirms localization itself is not a problem — the 1.43s you saw earlier was mainly network latency.

    However, application-configuration is still taking 1.45s locally, so let me ask a few questions:

    1. Is the BulkPermissionStore deployed in this local environment? Can you check the logs for BulkPermissionStore: cache hit or BulkPermissionStore: cache miss messages?
    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?
    3. 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)
  • User Avatar
    0
    vkelwan created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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:

    1. The built-in PermissionStore.IsGrantedAsync(string[]) takes the cached results and calls FormattedStringValueExtracter.Extract to 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.

    2. 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.cs with this updated version (added IsGrantedAsync override):

    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.cs and PermissionGrantBulkCacheItemInvalidator.cs) remain unchanged. Cache invalidation still works the same way.

    Please try these changes first and share the application-configuration response 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)
  • User Avatar
    0
    vkelwan created

    Hello this is on my local

    but on production server takes 3-4 secs why ?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 16, 2026, 14:50
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.