python-requestsraspberry-pifile-descriptor

Python Request Session throws connection errors after exactly six hours


TLDR: Python request session transmits data to API successfully for six hours and then suddenly only receives ConnectionErrors. Rate limits and other basic errors shouldn't be the issue as the application runs fine in the beginning.

I am using my Raspberry Pi 3 Model B V1.2 with a BME688 sensor to continuously transmit environmental data to openSenseMap. Here is the code of the application:

main.py:

RATE_LIMITS_TIME = 20 

def main() -> None:
    """
    Runs main process of recording and sending measurements to the OpenSenseMap.
    live sensor measurements every ten seconds due to rate limits
    """
    # create a single TCP connection
    session = requests.Session()
    
    while True:
    
        try:
            response = send_measurements_to_server(session)
        except requests.exceptions.ConnectionError:
            # renew session
            session = requests.Session()
            response = "-- CONNECTION FAILURE --"
        except requests.exceptions.Timeout:
            print("Request timed out, skipping this cycle")
            response = "-- TIMEOUT --"
         
        print(response,datetime.datetime.now())
        time.sleep(RATE_LIMITS_TIME)

send_to_server.py:

def send_measurements_to_server(session: requests.Session) -> int:

    opensensemap_url = "https://api.opensensemap.org/boxes/"
    adjusted_url = opensensemap_url + SENSE_BOX_ID + "/data"

    measurement = record_measurement()

    body = {HUMIDITY_SENSOR_ID: [measurement.humidity],
            TEMPERATURE_SENSOR_ID: [measurement.temperature],
            AIR_PRESSURE_SENSOR_ID: [measurement.pressure],
            GAS_RESISTANCE_SENSOR_ID: [measurement.gas_resistance]
            }
    
    response = session.post(adjusted_url, json=body, timeout=10) 

    return response

return_measurement.py:

def record_measurement() -> measurementClass:
    """Records and returns the measurement of the bme680 sensor"""

    try:
        sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)

        new_measurement = measurementClass(
                    sensor.data.temperature,
                    sensor.data.pressure,
                    sensor.data.humidity,
                    sensor.data.gas_resistance)
        
        print(sensor.data.temperature,
                    sensor.data.pressure,
                    sensor.data.humidity,
                    sensor.data.gas_resistance)
 
       
 
    except (RuntimeError, IOError):
        
        # if the sensor fails during data collection we use replacement values
        print("Sensor connection disrupted -- Using replacement values")
        
        new_measurement = create_artificial_measurement()   

    
    return new_measurement

The problem I am having is that for some reason the code runs successfully for pretty much exactly six hours. After this time period each post to the API returns a ConnectionError. This is especially weird since when I then start a second instance of my application, the server can be reached and the requests are accepted (while the first instance is still receiving Errors). This lets me believe that it isn't a network issue either.

Something to point out is that I use randomly generated data points in return_measurement.py if my sensor ever disconnects from the raspberry pi. This is done to ensure that there is always data being sent. This is of interest as it can be seen from the logfile that the sensor reconnects just before the ConnectionErrors start to be thrown:

logfile:

28.2 962.51 33.837 102400000.0
<Response [201]> 2025-05-14 00:38:49.823012
28.15 962.51 33.847 102400000.0
<Response [201]> 2025-05-14 00:39:09.983847
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:39:30.056937
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:39:50.175899
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:40:10.262512
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:40:30.330664
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:40:50.450122
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:41:10.604180
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:41:30.702610
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:41:50.801585
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:42:10.918886
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:42:31.072819
Sensor connection disrupted -- Using replacement values
<Response [201]> 2025-05-14 00:42:51.151268
Sensor connection disrupted -- Using replacement values
Request timed out, skipping this cycle
-- TIMEOUT -- 2025-05-14 00:43:21.174376
28.02 962.53 34.613 102400000.0
-- CONNECTION FAILURE -- 2025-05-14 00:43:41.249583
$ From here on out only Connection Failures $

I have tried lots of variations to get rid of this bug, I have tried to create a new session instance for each request, only using a single session instance for the whole application run, currently I only generate a new session instance when I receive a ConnectionError.

By now I am pretty clueless as to what causes the bug. Is there a possibility that this stems from a garbage collector on the raspberry pi?

Thanks for the help in advance ^_^


Solution

  • It turns out that the problem with this was the way I created sensor objects in return_measurements.py. The record_measurement() function would be called every twenty seconds creating a new sensor object, which would open a new file descriptor for the I2C device (sensor).

    On Unix-based systems like Raspberry Pi OS, processes are limited to 1024 file descriptors by default (ulimit -n), which was reached after ~6 hours (1024 requests × 20 seconds = ~20,480 seconds = 5.7 hours).

    I fixed this by only creating one sensor object which was created in the main file and then passed further:

    def main() -> None:
        """
        Runs main process of recording and sending measurements to the OpenSenseMap.
        sensor measurements posted every couple of seconds due to rate limits
        """
        # create a single TCP connection
        session = requests.Session()
    
        # initialize sensor ONCE
        sensor = init_sensor()
    
        while True:
        
            try:
                # PASS SENSOR OBJECT ON
                measurement = record_measurement(sensor)
                
                response = send_measurements_to_server(session, measurement)
            
            except requests.exceptions.ConnectionError:
                # renew session
                session = requests.Session()
                response = "-- CONNECTION FAILURE --"
    
            except requests.exceptions.Timeout:
                print("Request timed out, skipping this cycle")
                response = "-- TIMEOUT --"
             
            print(response,datetime.datetime.now())
            time.sleep(RATE_LIMITS_TIME)
    
    

    How I solved this:

    There were a few possible reasons for the error but I was able to rule out some of them.

    The remote closing of the connection by the server could have been possible, however as the API is associated with an application for which transmissions over multiple days are a regular use case, so this was pretty unlikely. Especially since I didn't get any other errors such as 429, etc.

    I knew that it wouldn't be a FileNotFoundError as I am not handling any files with my application and the error logs didn't indicate that.

    Through a bit of research I found the concept of file descriptors and was able to determine that my process overshot the limit. This command showed me how many file descriptors were used, which was over the standard limit of 1024.

    lsof -p <your-processId> | wc -l
    

    Finally, fixing my code was quite straightforward, as I simply adjusted the generation of the Session and Sensor object to be generated once instead of multiple times.