Server Sent Events using Laravel and Vue

Recently I was working on a new project, where I had to stream stock data and display it on a chart. My first instinct was to setup a loop using setTimeout, but I quickly realized that wouldn’t work for my exact needs. Stock prices change so frequently, often happening several times a second. Doing an ajax call every second seemed like the worst overhead baggage I could possibly program myself into. After doing a bit of research, I stumbled upon Server-Sent Events. 

Server-Sent Events is a web API for subscribing to a data stream sent by a server. Essentially this opens up a network request to the server that you can stream. Think of it like a Promise that never resolves. Implementing this API is fairly easy on the front-end using native JavaScript. For our tutorial, I’m going to wrap it in a VueJS instance.

Setting up the JavaScript Feed

The first thing we’ll do is setup our new Vue app.


const Vue = require('vue');

const app = new Vue({
    el: '#app',
    data: {
        stockData: null
    }
}

Once our app is created, we want to trigger a method for setting up the EventStream. Let’s call our method using the created() helper method that Vue provides.

Now we just need to implement that method. Let’s create our setupStream() method in the methods object. We’ll create a new EventSource and then add a listener for when new data comes in. When the new data comes in, we will update our data object for Vue.


created() {
    this.setupStream();
},
methods: {
    setupStream() {
        // Not a real URL, just using for demo purposes
        let es = new EventSource('http://awesomestockdata.com/feed');

        es.addEventListener('message', event => {
            let data = JSON.parse(event.data);
            this.stockData = data.stockData;
        }, false);
    }
}

It may be beneficial to know when the EventSource has closed for some reason, or if there was an error. In this case, we can add another event listener right below the first one.


es.addEventListener('error', event => {
    if (event.readyState == EventSource.CLOSED) {
        console.log('Event was closed');
        console.log(EventSource);
    }
}, false);

At this point we have a working EventSource feed collecting Server-Sent Events and storing the data into our Vue data model. Here is how our entire code should look.


const Vue = require('vue');

const app = new Vue({
    el: '#app',
    data: {
        stockData: null
    },
    created() {
        this.setupStream();
    },
    methods: {
        setupStream() {
            // Not a real URL, just using for demo purposes
            let es = new EventSource('http://awesomestockdata.com/feed');

            es.addEventListener('message', event => {
                let data = JSON.parse(event.data);
                this.stockData = data.stockData;
            }, false);

            es.addEventListener('error', event => {
                if (event.readyState == EventSource.CLOSED) {
                    console.log('Event was closed');
                    console.log(EventSource);
                }
            }, false);
        }
    }
});

Streaming Data Using Laravel

Laravel does support server-sent events out of the box. Your server may or may not need some extra configuration to get this to work. As of this writing I was using Nginx 1.10 with PHP 7.1, and everything worked for me out of the box.

To do this in Laravel we use Symfony’s StreamedResponse class. It does require a bit more configuration then what you are use to seeing in a clean Laravel app, but it works just the same. Let’s take a look at the code first, and then we’ll walk through what is happening here.


$response = new StreamedResponse(function() use ($request) {
    while(true) {
        echo 'data: ' . json_encode(Stock::all()) . "\n\n";
        ob_flush();
        flush();
        usleep(200000);
    }
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('X-Accel-Buffering', 'no');
$response->headers->set('Cach-Control', 'no-cache');
return $response;

The first thing we are doing is creating a new StreamedResponse class, and sending through our callback. In this callback, I’m running a simple endless loop, and querying the data. I’m using a basic Eloquent call here, but you should really use a Redis or some kind of fast data store if you are querying this much.

To finish off the loop, I’m waiting about 200ms to continue. Feel free to set this to whatever time interval your data needs to update.

Finally we set our headers. We need to tell the response to set the Content-Type to text/event-stream so your browser and JavaScript know what to do with it. The last two lines are for caching. I needed these to work on Nginx, but they may not be required for different Nginx setups, or for Apache.

Use With Caution

This solution really worked for me, and I would recommend it for anyone doing the same type of instant data streaming. Keep in mind that you are not running multiple ajax queries here. You are bundling everything into a single call. If that call fails, it will not restart.

The server will also be under above-average load. You won’t want to run this on your $5 Digital Ocean or Linode box. Remember that each one of your visitors will have an unending stream of data from your server, until they leave or close the browser. As always, plan your resources accordingly.

Comments

  1. has a matter of precaution, you should cap your connection to at list the execution time. It will prevent unecessary server side error. an behave more like a long polling (with less overhead)

    $start = time();
    $maxExecution = ini_get(‘max_execution_time’);
    $response = new StreamedResponse(function() use ($request, $start, $maxExecution) {

    while(true) {
    if(time() > = $start + $maxExectution) {
    exit(200);
    }

    echo ‘data: ‘ . json_encode(Stock::all()) . “\n\n”;
    ob_flush();
    flush();
    usleep(200000);
    }
    });
    $response->headers->set(‘Content-Type’, ‘text/event-stream’);
    $response->headers->set(‘X-Accel-Buffering’, ‘no’);
    $response->headers->set(‘Cach-Control’, ‘no-cache’);
    return $response;

      1. Why would that be necessary? It’s supposed to be a channel between my app and the client, for as long as she is online. Why would I “cap” the execution time? Is it because otherwise the server side code would run forever, even if the client disconnects? If so, we need a better solution.

      2. well as you can’t catch a max execution time error (as the script simply stop and the error is handled by the web server rather than php). This not the “best” solution but it at list prevent the front to close definitely the connection. On the other hand, if you close it properly it will reopen itself automatically.

  2. Thanks for writing this, good post! I discovered it after writing a similar one on “Streaming an Ajax response with Vue.js and Server-sent events (SSE)”:

    https://www.strehle.de/tim/weblog/archives/2017/06/02/1619

    By the way, there’s a small typo in your example – I think it should be “Cache-Control”.

    Have you had to support IE or Edge browsers, too? They say there’s EventSource polyfills for them but I haven’t tried them out yet.

  3. small last note you should add a 2k padding on top for IE.

    $start = time();
    $maxExecution = ini_get(‘max_execution_time’);
    $response = new StreamedResponse(function() use ($request, $start, $maxExecution) {

    while(true) {
    if(time() > = $start + $maxExectution) {
    exit(200);
    }

    echo “:” . str_repeat(” “, 2048) . “\n”; // headers->set(‘Content-Type’, ‘text/event-stream’);
    $response->headers->set(‘X-Accel-Buffering’, ‘no’);
    $response->headers->set(‘Cach-Control’, ‘no-cache’);
    return $response;

  4. as this is for laravel, you can use

    “`
    return response()->stream(function () {
    while(true) {
    echo ‘data: ‘ . json_encode(Stock::all()) . “\n\n”;
    ob_flush();
    flush();
    sleep(0.2);
    }
    }, 200, [
    ‘Content-Type’ => ‘text/event-stream’,
    ‘X-Accel-Buffering’ => ‘no’,
    ‘Cache-Control’ => ‘no-cache’,
    ]);
    “`

  5. It would be lovely if you did a sequel to this blog post, but using laravel broadcasts and mercure (open source SSE pusher written in golang)

  6. The problem is when the user up to 5 request to the server, the server’s cup is going up to 90% and cannot request anymore! I am using this tutorial and test and get this server problem error! Can u help me how to solve it?

  7. hi when I use this code “laravel” became freeze and I can not do other function until I get this message
    Maximum execution time of 60 seconds exceeded
    and loop stop after this other function can be run

  8. IF you miss one of these you will get that freezy app:
    ..(no magic part).. then :
    echo ‘data: ‘ . json_encode($whatAmSending) ,”\n\n”;
    flush();
    ob_flush();
    ob_end_flush();
    ob_end_clean();
    usleep(5000000);
    if (connection_aborted()) {
    break;
    }

  9. How do you setup the route for the particaulat controller function which return the StreamedResponse?

Leave a Reply

Your email address will not be published. Required fields are marked *