27

I'm using Firebase Functions with https triggers, and I was wondering how long after sending the response to the client, the functions keeps executing. I want to send a response to the client and then perform another operation (send a mail). Currently I'm doing this as following:

module.exports.doSomeJob = functions.https.onRequest((req, res) => {
    doSomeAsyncJob()
    .then(() => {
        res.send("Ok");
    })
    .then(() => {
        emailSender.sendEmail();
    })
    .catch(...);
});

The above code is working for me, but I'm suspecting that the code only works because sending the mail has finished before the res.send has completed, so I was wondering how exactly the termination process is working to make sure the code will not break.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
atlanteh
  • 5,615
  • 2
  • 33
  • 54

3 Answers3

36

You should expect that the HTTP function terminates the moment after you send the response. Any other behavior is some combination of luck or a race condition. Don't write code that depends on luck.

If you need to send a response to the client before the work is fully complete, you will need to kick off a second function to continue where the HTTP function left off. It's common to use a pub/sub function to do with. Have the HTTP function send a pub/sub message to another function, then terminate the HTTP function (by sending a response) only after the message is sent.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • 1
    Hé this does seem to be the only way to do this. But if I look at documentation for pub/sub I only seem to find the subscribing side, but never how to call it from a cloud function. Could you extend your answer to include how to actually do this? – Daan Luttik Aug 08 '18 at 12:16
  • 2
    You can use the Google cloud pubsub client to send the message. I haven't done it myself. https://cloud.google.com/nodejs/docs/reference/pubsub/0.16.x/ – Doug Stevenson Aug 08 '18 at 16:16
  • @DougStevenson - could you please add an example? – Unnikrishnan Apr 20 '19 at 13:48
  • 3
    Here's a reference to the Google documentation where this is stated explicity, "..the code executed after sending the HTTP response could be interrupted at any time" https://cloud.google.com/functions/docs/concepts/exec#function_execution_timeline – Timothy Johns Apr 07 '20 at 23:26
  • 1
    @Unnikrishnan there is a nice example here: https://stackoverflow.com/a/61525321/867631 – Lauren ten Hoor Dec 05 '20 at 12:00
  • 1
    Problem with this solution is the pub/sub can take 2 mins to fire - seeing this in production. – nbransby Dec 30 '20 at 12:33
11

If the expected response is not pegged to the outcome of the execution, then you can use

module.exports.doSomeJob = functions.https.onRequest((req, res) => {
res.write('SUCCESS')
return doSomeAsyncJob()
.then(() => {
    emailSender.sendEmail();
})
.then(() => {
    res.end();
})
.catch(...);
});

This sends back a response a soon as a request is received, but keeps the function running until res.end() is called Your client can end the connection as soon as a response is received back, but the cloud function will keep running in the background Not sure if this helps, but it might be a workaround where the client needs a response within a very limited time, considering that executing pub/sub requires some extra processing on its own and takes time to execute

Kisinga
  • 1,640
  • 18
  • 27
  • 1
    This is perfect, thank you!! Use case for me is Slack, which errors out if it doesn't receive a `200` status response in 300 ms. This seems to fix the issues I was having. – Andrew Schwartz Oct 29 '19 at 03:30
  • I'm glad it helped – Kisinga Oct 30 '19 at 05:51
  • Is the connection kept alive for the client / caller ? – Jimmy Kane Jul 13 '20 at 14:33
  • @jimmy-kane Although I haven't experimented to verify this, I would presume that to be the case, because we're calling re.end later. Please validate my assumption though. In case this is a desired behaviour, you could also consider injecting a middleware to achieve this functionality refer to https://firebase.google.com/docs/functions/http-events – Kisinga Jul 13 '20 at 15:09
  • 3
    This will not always work, and will probably fail more often than not. Cloud Functions clamps down on computing resources as soon as the response is sent to the client, and there is absolutely no guarantee that any pending async work will complete. – Doug Stevenson Dec 30 '20 at 17:27
  • @DougStevenson the assumption is that sending partial results works, and the cloud function is detected as "finished" when we end the response – Kisinga Dec 31 '20 at 09:33
  • 2
    That assumption does not hold in practice. The function terminates immediately after the response is sent. There is no such thing as a "partial response". The response is buffered in memory and sent entirely in one shot. You might have a fraction of a second to compute something else, but that should not be depended on. I strongly suggest trying this for yourself to see that it doesn't really work. – Doug Stevenson Dec 31 '20 at 16:51
  • So what the correct way to deal with webhooks that timeout after a very short amount of time ? In my case, I need to store the webhook payload in a collection that triggers a function that will actually do the logic. But saving the payload takes time. So I need to respond as soon as I receive the response but then how do I save the payload correctly ? – Mehdi Feb 09 '21 at 23:19
  • @Mehdi as long as the cloud function invocation is idempotent, im thinking you can get away with letting the service retry. MOst retries happen after a short time and the second time the invocation is "hot" and the execution is much faster. Therefore, the response can be given much faster. Due to indepotency, the result of execution doesnt cause errors or duplicate results. This is a hack but might be better than creating a proxy, which would be a better solution but beats the purpose of having a cloud function in the first place – Kisinga Feb 16 '21 at 06:47
  • @Kisinga I am not sure I understand. I have an endpoint that a webhook talks to. webkhook delivers a payload and needs a 200 response within 150ms. I can do that if I don't store the payload. But then it's useless. Or I store the payload and then respond which takes more than 150ms. Or I can start saving the payload without waiting for the task to end but then I am not sur if it will execute correctly. Not to mention that such a code won't deploy as I am not handling the promise correctly – Mehdi Feb 16 '21 at 15:24
  • @DougStevenson is this documented? Given that the node instances usually keep running (for example if admin.initializeApp is called unconditionally again by another invocation without guarding, we get an error), what exactly kills the function and stops it from eventually returning normally? I would have expected it to run until it returns naturally or it violates a time constraint. Why kill it early? – Chris Chiasson Jun 19 '23 at 16:43
  • @ChrisChiasson Because Google doesn't want to execute CPU cycles on something that's not being paid for. Best to just stop/kill anything that's not following the expected usage and billing behavior. If you need out-of-band background processing, use a different product that supports and bills it as you would expect. – Doug Stevenson Jun 19 '23 at 18:24
  • @DougStevenson I'm not sure I would characterize it as out of band background processing so much as just a standard HTTP response. I wanted to reply to a call with 200 and send back plain text that is progressively generated until finished rather than forcing the user to wait with no results until the entire response was done. Perhaps doable with v2? – Chris Chiasson Jun 29 '23 at 22:14
5

TL;DR

While https functions will terminate shortly after res.send(), it is not guaranteed that 0 lines of code after res.send() will be executed.

I think a fuller answer has 2 components:

  1. as Doug pointed out, do not put any additional code you expect to be executed after res.send()
  2. cloud functions will terminate shortly after res.send(), but don't expect that exactly 0 lines of code will be executed

I ran into a situation where for a db maintenance script, if no records met my criteria, I said goodbye with res.send() and had additional logic after it. I was expecting that piece not to be run, since I've already terminated the request.

Example producing unexpected results:

exports.someFunction = functions.https.onRequest((req, res) => {
  if (exitCriteria === true) {
    // we can exit the function, nothing to do
    console.log('Exit criteria met')
    res.status(200).send()
  }

  // code to handle if someCriteria was falsy
  console.log('Exit criteria not met, continue executing code')
})

In the above example, I was expecting res.send() to terminate the function immediately - this is not so, the second console.log may also be hit - along with any other code you may have. This is not guaranteed, however, so execution may abruptly stop at some point.

Example producing correct results:

exports.someFunction = functions.https.onRequest((req, res) => {
  if (exitCriteria === true) {
    // we can exit the function, nothing to do
    console.log('Exit criteria met')
    res.status(200).send()
  }
  else {
    // code to handle if someCriteria was falsy
    console.log('Exit criteria not met, continue executing code')
  }
})

In this version, you will see exactly 1 line of console.logs - as I was originally intending.

anothermh
  • 9,815
  • 3
  • 33
  • 52
Ashton
  • 1,265
  • 14
  • 23