# AWS Lambda와 Connection Pool 사용 시 발생한 응답 지연 문제 해결기

## 배경

> 개발자님! 화면이 너무 늦게 떠요!!

이번 주에 매출 통계 기능을 개발해서 개발 서버에 올렸는데 테스팅을 하다가 갑자기 긴급 호출이 들어왔다. API 속도가 너무 느리다는데 얼마나 느리길래 그런지 확인해봤더니 무려 10~16초가 걸렸다.

신규 레포지토리를 파서 개발한 거라 설정에 문제가 있었나? VPC 간 통신 때문에 지연되는 건가? 별의별 생각이 다 들어서 각 모듈별로 실행되는 시간, 실제 쿼리하는 시간을 전부 측정해봤는데 **10~16초나 걸릴 만한 이유를 찾지 못했다.**

실제 동작이 query string 받아서 `SELECT * From table WHERE a = ? AND b = ? ORDER BY c DESC` 정도만 수행하는 아주 간단한 API 였는데, 너무 이상한 수치였다.

CloudWatch 로그를 아무리 뒤져봐도 에러도 보이지 않았고,  
코드의 각 부분이 실행되는 시간을 측정해봐도 **2초 이내에 모든 실행이 완료**되고 있었다.

이상한 점은,

* 지정한 동작(쿼리 실행, 결과 가공 등)은 모두 끝났는데도
    
* 응답을 바로 반환하지 않고 6초 이상 대기한 뒤에야 응답을 반환하고 있다는 것
    

이었다.

즉, **“로직은 다 끝났는데, 어딘가에서 응답이 붙잡혀 있는 상태”** 였다.

## Connection Pool과 유휴 연결

결론부터 말하면, 문제의 1차적인 원인은

> **connection pool의 유휴 연결 때문에 Node.js 이벤트 루프가 완전히 비지 않았고**,  
> **그로 인해 Lambda가 응답을 바로 반환하지 못하고 지연이 발생한 것**이었다.

사용하고 있던 라이브러리는 [pg.Pool](https://node-postgres.com/apis/pool)이었고, idleTimeoutMillis 기본값은 10초다.  
node-postgres 문서의 설명은 다음과 같다:

> number of milliseconds a client must sit idle in the pool and not be checked out  
> before it is disconnected from the backend and discarded  
> **default is 10000 (10 seconds)** – set to 0 to disable auto-disconnection of idle clients

즉, 한 번 빌려갔다가 돌려준 커넥션이 **최소 10초 동안 idle 상태로 pool 안에 남아 있고**, 이 동안에는 소켓/타이머 레퍼런스가 살아 있기 때문에 **Node.js 이벤트 루프 입장에서는 “아직 완전히 idle이 아니다” 라고 보는 것**이다.

## Lambda handler 방식과 이벤트 루프의 관계

여기서 자연스럽게 이어지는 질문은 이것이다.

> “idle connection이 이벤트 루프를 잡고 있는 건 알겠는데,  
> 이벤트 루프가 안 비면 왜 응답이 늦게 나가는 걸까?”

그 이유는 **Lambda handler 방식** 때문이다.

Node.js에서 Lambda handler는 크게 두 가지 방식이 있다.

1. **async/await 기반**
    
    ```javascript
    exports.handler = async (event, context) => {
      const result = await doSomething(event);
      return {
        statusCode: 200,
        body: JSON.stringify(result),
      };
    };
    ```
    
2. **callback 기반** [(공식 문서 기준으로 Node.js 22.x까지만 지원된다)](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html)
    
    ```javascript
    exports.handler = (event, context, callback) => {
      doSomething(event, (err, result) => {
        if (err) {
          return callback(err);
        }
        callback(null, {
          statusCode: 200,
          body: JSON.stringify(result),
        });
      });
    };
    ```
    

callback 방식에서는 기본적으로

* 콜백을 호출한 뒤에도
    
* **이벤트 루프에 남아 있는 작업(타이머, 소켓 등)이 모두 사라질 때까지**,
    
* 또는 **함수 타임아웃에 도달할 때까지**
    

실행을 계속 유지하려는 동작을 한다.

그래서 callback 방식으로 구현한 상태에서 이벤트 루프를 붙잡고 있는 idle connection이 있으면,  
**로직은 이미 끝났는데도 응답이 바로 나가지 않고 지연되는 상황이 만들어질 수 있다.**

## **fastify-aws-lambda 내부는 callback 기반이었다**

Fastify 앱을 Lambda로 올릴 때 우리는 아래와 같은 형태로 간단하게 래퍼만 호출하고 있었다.

```javascript
const awsLambdaFastify = require('@fastify/aws-lambda');
const { createApp } = require('./createApp');

const app = createApp();
const proxy = awsLambdaFastify(app);

module.exports.handler = proxy;
```

코드만 보면 이 핸들러가 **async/await 기반인지, callback 기반인지 전혀 알 수 없다.**

그런데 내부 구현을 확인해보니, [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify)는 Fastify 앱을 감싸면서 **최종적으로 Lambda에 등록되는 handler를 (event, context, callback) 형태의 callback 기반으로 생성하고 있었다.**

그 결과,

* callback 스타일의 \*\*기본 동작(이벤트 루프가 완전히 빌 때까지 실행을 유지하려는 동작)\*\*이 그대로 적용되고
    
* `pg.Pool`의 idle connection이 이벤트 루프를 비우지 못하는 상황과 맞물리면서
    
* **로직은 이미 다 끝났는데도 응답이 수 초 동안 지연되는 현상**이 발생했다.
    

## 해결방법

### 1) Lambda context.callbackWaitsForEmptyEventLoop = false

먼저, Node.js Lambda의 [Context 객체](https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/nodejs-context.html)는

함수 이름, 버전, 메모리 등 실행 환경에 대한 정보와 몇 가지 설정 값을 제공하는데,  
그중 하나가 `callbackWaitsForEmptyEventLoop`다. 이 값은 **callback 기반 핸들러에서만 의미가 있는 옵션**이다.

문서 설명을 보면:

> Node.js 이벤트 루프가 빌 때까지 대기하는 대신, 콜백이 실행될 때 즉시 응답을 보내려면 false로 설정합니다.  
> 이것이 false인 경우, 대기 중인 이벤트는 다음 번 호출 중에 계속 실행됩니다.

즉, 이 값을 `false`로 설정하면

* **이벤트 루프가 완전히 비기까지 기다리지 않고**
    
* **이미 준비된 응답을 바로 반환**하고
    
* 남아 있는 타이머/소켓 같은 것들은 **다음 호출에서 재사용될 수 있도록 놔두는**
    

동작을 하게 된다.

Fastify를 Lambda로 올릴 때는 `aws-lambda-fastify`를 통해 handler를 만들고 있었기 때문에,  
이 옵션을 래퍼에 직접 넘겨서 동작을 바꿔줄 수 있었다.

```javascript
const proxy = awsLambdaFastify(app, {
  callbackWaitsForEmptyEventLoop: false,
});

module.exports.handler = proxy;
```

이렇게 설정한 이후에는 로직이 끝난 시점과 클라이언트가 응답 받는 시점이 거의 일치하게 바뀌었고,  
10초 가까이 붙잡혀 있던 지연 현상도 없어졌다.

### 2) 유휴 연결시간 줄이기

유휴 연결시간 자체를 줄여서 이벤트 루프에 idle 연결이 더 짧게 남도록 조정하는 방법도 시도했봤다.

```js
const { Pool } = require('pg');

const pool = new Pool({
  idleTimeoutMillis: 1000, // 1초로 설정
});
```

기본값(10초)으로 설정했을 때보다 확실히 응답 시간이 줄어들긴 했지만,  
여전히 응답이 준비된 이후에 잠깐의 대기 시간이 생기는 문제가 있었다.

그렇다고 idleTimeoutMillis를 너무 짧게 설정하면,  
비용이 큰 데이터베이스 연결을 Lambda가 실행될 때마다 계속 새로 시도해야 하는 문제가 생긴다.

결국,

* `context.callbackWaitsForEmptyEventLoop = false`로 응답을 먼저 보내도록 만들고
    
* `idleTimeoutMillis`는 사용 패턴에 맞는 적절한 값으로 조정하는 것
    

이 현실적인 타협점이라고 판단했다.

## 결론

삽질했던 시간보다 훨씬 간단하게 해결한 문제였다.

이번에 겪은 일은, 결국 내가 사용하고 있는 플랫폼에 대한 이해 부족에서 비롯된 문제였다. 요즘은 AWS 서비스들이 워낙 잘 되어 있다 보니 “설마 인프라 쪽에 원인이 있겠어?”라는 생각으로, 코드만 붙잡고 있었던 것도 한몫했다. 앞으로는 코드에만 집중하기보다, 런타임·플랫폼·네트워크까지 포함해서 문제를 좀 더 다각도로 바라보려 한다.

이번에 크게 느낀 점은 두 가지다.

* **첫째, 라이브러리의 겉 API만 보지 말고, 중요한 경계(Lambda handler, DB 커넥션, 이벤트 루프 등)를 다루는 부분의 구현은 한 번쯤 직접 뜯어보자**는 것이다. 이번에도 aws-lambda-fastify 내부가 callback 기반이라는 사실을 알고 나서야 퍼즐이 맞춰졌다.
    
* **둘째, 서버리스를 사용할 때는 기본 동작과 내가 적용한 설정이 어떻게 맞물리는지 반드시 이해**해야 한다. Lambda의 실행 모델, handler 방식(async/await vs callback), Connection Pool의 동작 방식, idleTimeoutMillis, callbackWaitsForEmptyEventLoop 같은 설정은 실제 성능과 응답 지연에 직접적인 영향을 준다. “서버리스니까 알아서 처리해주겠지”라는 기대보다는, 내 코드가 서버리스 환경에서 어떻게 실행되는지 명확히 이해하고 사용하는 것이 중요하다는 점을 다시 한 번 느꼈다.
    

이런 것들을 한 번 겪어두면, 다음 비슷한 장애가 왔을 때 “코드만 들여다보는” 시간은 조금 줄어들 거라고 기대하고 있다.

## 참고자료

* [NodeJS 와 PostgreSQL Connection Pool](https://jojoldu.tistory.com/634)
    
* [AWS Lambda RDS connection timeout](https://stackoverflow.com/questions/42605093/aws-lambda-rds-connection-timeout)
    
* [https://gist.github.com/streamich/6175853840fb5209388405910c6cc04b](https://gist.github.com/streamich/6175853840fb5209388405910c6cc04b)
    
* [https://docs.aws.amazon.com/ko\_kr/lambda/latest/dg/nodejs-context.html](https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/nodejs-context.html)
    
* [https://dev.to/dvddpl/event-loops-and-idle-connections-why-is-my-lambda-not-returning-and-then-timing-out-2oo7](https://dev.to/dvddpl/event-loops-and-idle-connections-why-is-my-lambda-not-returning-and-then-timing-out-2oo7)
    
* [https://en.wikipedia.org/wiki/Connection\_pool](https://en.wikipedia.org/wiki/Connection_pool)
