1

I have a Flask site deployed to IIS via wfastcgi configuration.

When I use chrome or firefox developer tools to analyse the loading time of the homepage, I find many seconds (ranging from 6 to 10 in average) as waiting time to receive the first byte.

It was even 30 seconds before, but then I "optimized" my python code to avoid any db sql operation at loading time. Then I've followed the hints of this blog of nspointers, and now from the taskbar of the server I see the w3wp.exe for my app pool identity

w3wp.exe – It is the IIS worker process for the application pool

staying up and running even during idle time. But that is not true for the other

python.exe – The main FastCGI process for the Django or flask applications.

and I'm not sure if this is a problem and just in case what I am supposed to do, aside from the step 4 described in the mentioned post.

Now in the “Edit FastCGI Application” dialog under “Process Model” edit the “Idle Timeout” and set it to 2592000 which is the max value for that field in seconds

I've also looked at the log written by the Flask app and compared it to the log written by IIS and this is the most important point in making me believe that the issue is in the wfastcgi part, before the execution of the python code.

Because I see that the time-taken of the IIS log matches with the client time reported by chrome or firefox as TTFB and the log written by python at the start of the execution is logged at almost the same time of the time written by IIS, that

corresponds to the time that the request finished

(as I thought indeed and as I find it's confirmed by this answer)

So in conclusion, based on what I tried and what I understand, I suspect that IIS is "wasting" many seconds to "prepare" the python wfascgi command, before actually starting to execute my app code to produce a response for the web request. It is really too much in my opinion, since other applications I've developed (for example in F# WebSharper) under IIS without this mechanism of wfastcgi load immediately in the browser and the difference in the response time between them and the python Flask app is quite noticeable. Is there anything else I can do to improve the response time?

2 Answers2

1

Ok, now I have the proof I was searching and I know where the server is actually spending the time. So I've researched a bit about the wfastcgi and finally opened the script itself under venv\Lib\site-packages.

Skimming over the 900 lines, you can spot the relevant log part:

def log(txt):
    """Logs messages to a log file if WSGI_LOG env var is defined."""
    if APPINSIGHT_CLIENT:
        try:
            APPINSIGHT_CLIENT.track_event(txt)
        except:
            pass
    
    log_file = os.environ.get('WSGI_LOG')
    if log_file:
        with open(log_file, 'a+', encoding='utf-8') as f:
            txt = txt.replace('\r\n', '\n')
            f.write('%s: %s%s' % (datetime.datetime.now(), txt, '' if txt.endswith('\n') else '\n'))

Now, well knowing how to set the environment variables, I defined a specific WSGI_LOG path, and here we go, now I see those 5 seconds TTFB from chrome (as well as the same 5 seconds from IIS log with time 11:23:26 and time-taken 5312) in the wfastcgi.py log.

2021-02-01 12:23:21.452634: wfastcgi.py 3.0.0 initializing
2021-02-01 12:23:26.624620: wfastcgi.py 3.0.0 started

So, of course, wfastcgi.py is the script one would possibly try to optimize...

BTW, after digging into it, that time is due to importing the main flask app

handler = __import__(module_name, fromlist=[name_list[0][0]])

What remains to be verified is the behavior of rerunning the process (and the import of the main flask module, that is time consuming) for each request.

In conclusion, I guess it is a BUG, but I have solved it by deleting the "monitoring changes to file" FastCGI settings as per the screenshot below.

enter image description here

The response time is under a second.

0

I have a different answer to you by suggesting you try to switch over to HTTP Platform Handler for your IIS fronted Flask app.

Config Reference

This is also the recommended option by Microsoft:

Your app's web.config file instructs the IIS (7+) web server running on Windows about how it should handle Python requests through either HttpPlatform (recommended) or FastCGI.

https://learn.microsoft.com/en-us/visualstudio/python/configure-web-apps-for-iis-windows?view=vs-2019

Example config can be:

<configuration>
  <system.webServer>
    <handlers>
      <add name="httpplatformhandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
    </handlers>
    <httpPlatform processPath="c:\inetpub\wwwroot\run.cmd" 
                  arguments="%HTTP_PLATFORM_PORT%" 
                  stdoutLogEnabled="true" 
                  stdoutLogFile="c:\inetput\Logs\logfiles\python_app.log"
                  processPerApplication="2"
                  startupTimeLimit="60"
                  requestTimeout="00:01:05"
                  forwardWindowsAuthToken="True"
                  >
      <environmentVariables>
        <environmentVariable name="FLASK_RUN_PORT" value="%HTTP_PLATFORM_PORT%" />
      </environmentVariables>
    </httpPlatform>
  </system.webServer>
</configuration>

With run.cmd being something like

cd %~dp0
.venv\scripts\waitress-serve.exe --host=127.0.0.1 --port=%1 myapp:wsgifunc

Note that the HTTP Platform handler will dynamically set on a port and passing that into the python process via the FLASK_RUN_PORT env var which flask will automatically take as a port configuration.

Security notes:

  • Make sure you bind your flask app to localhost only, so it's not visible directly from the outside - especially if you are using authentication via IIS
  • In the above example the forwardWindowsAuthToken is being set which then can be used to rely on Windows Integrated authentication done by IIS then the token passed over to Python and you can get the authenticated user name from Python. I have documented that here. I actually use that for single-sign on with Kerberos and AD group based authorization, so it works really nice.

Example to only listen on localhost / loopback adapter to avoid external requests hitting the python app directly. In case you want all requests to go via IIS.

if __name__ == "__main__":
    app.run(host=127.0.0.1)
CJ Harmath
  • 1,070
  • 7
  • 15
  • Interesting ... I have Windows Integrated authentication but without single sign on in this case, so I have to test if this works well also in my scenario. And what are you actually using in production instead of Flask’s built-in server (that is not suitable for production)? waitress maybe? (btw do you know or can you suggest which are the technical reasons why they recommended HttpPlatform over FastCGI? To be sure they fully apply to my specific case as well) –  Feb 25 '21 at 00:18
  • I've switched over to FastAPI and not going back :) – CJ Harmath Feb 25 '21 at 02:02
  • Using it with uvicorn behind IIS on windows because of single sign-on and AD groups bases authorization. HTTP Platform handler is developed by Microsoft and it was first used to run ASP.NET core then it evolved to be a generic handler. You can expect higher quality, performance and stability, but I can't back this up. Works fine for me. – CJ Harmath Feb 25 '21 at 02:28
  • Ok, in a few hours I'll be doing a few tests on my dev server in the intranet with my domain userid and with the actual code of my flask project, by copying the IIS directory under a new virtual path... I hope the HttpPlatform is already installed there and I can quickly finish the whole setup and eventually find a fast response time from the browser to assign the bounty... Thanks for the above reply. –  Feb 25 '21 at 04:18
  • sorry, not able to make this work... I'm not sure if it is due to the fact that I'm using a venv or to some mistakes in my web config that I'm not able to spot... Is HTTP_PLATFORM_PORT a sys env var defined as e.g. 5000 ? And do you restart IIS after defining it? Also, even if that is solved, I'm not sure about the TLS part, for example I read that waitress would not be natively supporting it... But I'm stuck before that –  Feb 25 '21 at 15:51
  • First validate that you have a correct cmd file which can startup on the port defined by the FLASK_RUN_PORT env var i.e.: SET FLASK_RUN_PORT=1234 run.cmd If it works and it's listenning on port 1234, then IIS will send the traffic to that. You might need to recycle the app pool, yep. If it doesn't work try to figure out what's the correct path. TLS will be terminated on the IIS, then it goes HTTP to flask. – CJ Harmath Feb 26 '21 at 05:13
  • example run.cmd assuming venv under .venv - change according to your setup cd %~dp0 .venv\scripts\waitress-serve.exe --host=127.0.0.1 --port=%FLASK_RUN_PORT% myapp:wsgifunc First line changes the work dir to the scripts path, so that you can use relative .\ path, then it runs waitress with localhost and the dynamically assigned port. Change myapp:wsgifunc to match your setup too. – CJ Harmath Feb 26 '21 at 05:27
  • updated the run.cmd in the answer as these comments won't have line breaks. also changed the web.config to pass the dynamic port which the run.cmd can take as the first parameter and spin up the server on that port. – CJ Harmath Feb 26 '21 at 05:31
  • The command is working as standalone but from IIS I get "502 - Web server received an invalid response while acting as a gateway or proxy server" (and I'm not sure if it actually starts the command or it stops before for some error...). My curiosity: Which is your binding: do you use a real certificate and do you have an hostname with its own subnetwork and dns? However I've tried for hours, anything, I could think of: https, plain http, different hostnames or only ip, ... Have you installed the httpPlatformHandler_amd64.msi ? I'm on Windows 2012 R2 with IIS 8.5 –  Feb 26 '21 at 08:15
  • And settings of app pool maybe? .net clr version? startup mode? but also in this case I've tried different possibilities with no luck –  Feb 26 '21 at 08:27
  • For the record, I'm using a "No Managed Code" app pool w/ Start Mode "AlwaysRunning". All is working ok and finally also the win auth token is received by the http platform handler as I've commented in your linked answer (a few more details there). The response time is fast as well. –  Feb 26 '21 at 13:29